Skip to content
DedicatedPHP Contact

Out-of-Order Webhooks in PHP Without Corrupting State

Design a PHP integration resilient to duplicate, delayed, and concurrent events through validation, auditing, and idempotency.

Editorial diagram of duplicate and delayed webhook events processed by a PHP application with state control

Out-of-order webhooks in PHP are a consistency problem, not just a connectivity issue. A provider may resend a delivery because it did not receive a valid response, a queue may delay a message, or two events for the same entity may travel through different routes. If the application assumes that each event arrives once and in sequence, an old confirmation can overwrite a later cancellation, or a retry can execute an irreversible operation twice.

The starting rule is simple: a webhook is a notification that something may have changed in another system. It is not, by itself, a reliable instruction to mutate local state without checks. The design must preserve the received evidence, decide which events are admissible, and apply changes idempotently and in order according to domain rules.

Separate receipt, validation, and domain application

Separate receipt, validation, and domain application — DedicatedPHP visual guide

The HTTP endpoint should do little and do it predictably. Its responsibility is to receive the request, verify it, persist an immutable record, and respond within the time expected by the sender. Work that modifies orders, subscriptions, inventory, or any other business entity should happen afterward, usually through an asynchronous process.

Separating phases prevents a transient failure in an internal API from turning a valid delivery into an ambiguous retry. It also makes it possible to resume processing without asking the provider to resend old events.

  1. Receipt: capture headers, the unmodified body, the receipt timestamp, and the identified source.
  2. Input validation: check the signature, format, size, content type, and minimum fields.
  3. Persistence: store the event and its initial state in a short transaction.
  4. Queueing: signal that pending work exists, without relying on processing it within the HTTP response.
  5. Application: a worker interprets the event, obtains the required state, and performs a controlled business transition.

It is important to distinguish a delivery from an event. The same delivery can be repeated, and some providers assign a different identifier to each delivery attempt. If a stable event identifier exists, it is usually the best basis for deduplication. If none exists, a key must be defined using the source, external entity, type, and a version or timestamp with known meaning.

What to record for auditing and reprocessing

An event table must not store only the interpreted JSON. Keep the original body, because normalizing it before storage may remove information needed to verify a signature, investigate an incident, or adapt a later parser.

At a minimum, the record should contain:

  • Source or provider and integration environment.
  • External event identifier and, if available, delivery identifier.
  • Event type, external entity identifier, and version, sequence, or effective date.
  • Relevant headers and the original payload protected against modification.
  • Local receipt timestamp and, separately, the timestamp declared by the sender.
  • Cryptographic fingerprint of the payload for diagnostics and auxiliary deduplication.
  • Processing status: received, validated, pending, applied, ignored, failed, or under review.
  • Number of attempts, summarized error, last attempt timestamp, and reference to the affected local entity.

A unique constraint on (source, external_event_id) resolves repetition when the provider offers a stable ID. Insert first and treat the conflict as an already known delivery, not as a business error. The response can still be successful to stop retries.

But deduplicating the message is not enough to guarantee idempotency. For example, two different events may express the same confirmation and both attempt to create an accounting entry. The business operation must have its own protection: an idempotency key, a unique constraint on the effect, or a transition that checks whether the result already exists.

Validate authenticity and limit the input surface

Do not accept a webhook because it comes from an expected IP address or because it includes a field that looks secret. When the provider allows it, validate a signature calculated over the raw body and a timestamp. The comparison must be constant-time, and the time window must limit replays while accounting for reasonable clock skew.

Before persisting, enforce operational limits: maximum body size, read timeout, accepted formats, and a minimum schema. Valid JSON is not necessarily a valid event. Reject unknown types unless there is an explicit policy to archive them without applying effects.

Signing secrets require rotation. During a change, it may be necessary to accept an old key and a new key for a defined period, recording which one validated the delivery. Do not include full bodies, tokens, or unnecessary personal data in application logs. The audit record must have access controls and a retention policy appropriate to the sensitivity of the data.

Decide logical order, do not trust network order

The receipt time does not define what happened first. Nor is a date included in the payload always sufficient: it may be approximate, belong to event creation rather than the transition, or be affected by unsynchronized clocks. The best signal is a monotonic version or a sequence number per entity provided by the source system.

When a version exists, store the last applied version on the local entity. A worker can apply an event only if its version is greater than the stored one; an equal version indicates repetition, and a lower one is a delayed event. If there are sequence gaps, do not invent the intermediate state: mark the entity for reconciliation or query the source API, if that API is the system of record.

If there is no sequence or version, the rules must come from the domain. An explicit state machine is safer than directly assigning received text. For example, a cancelled entity could be prevented from returning to confirmed unless there is a documented and authorized transition. The model must define what to do with each combination of current state and incoming event.

if ($eventVersion <= $entity->lastExternalVersion) {
    markIgnored($event, 'version_no_mas_reciente');
    return;
}

applyAllowedTransition($entity, $event);
$entity->lastExternalVersion = $eventVersion;

The code illustrates the criterion; it does not replace the transaction or the transition rules. For events without a version, a date comparison is acceptable only if the sender's contract guarantees its semantics and precision.

Handle delayed events according to the cost of being wrong

Not all delayed events deserve the same response. Choosing whether to ignore, record, recalculate, or compensate depends on whether the event can change a real obligation and on which system is the source of truth.

  • Ignore: appropriate for an old version whose effect is already included in a verifiable later state.
  • Record and alert: useful if the sequence is inconsistent or information is missing to decide without intervention.
  • Recalculate: query the current state in the external system and update the local mirror when the external source prevails.
  • Compensate: create a traceable corrective action when a previous effect has already produced consequences and cannot be safely removed.

Consider a hypothetical case involving an external operation. A confirmation with version 12 arrives, followed by a cancellation with version 13, and later confirmation 12 is retried. With version control, the retry does not revive the operation. If the cancellation arrives first and the system knows that version 12 is missing, it can apply the cancellation if the state machine allows it or request reconciliation before producing a sensitive effect.

Internal concurrency, queues, and per-entity locks

Asynchronous processing improves responsiveness, but it introduces internal races: two workers can read the same state before one writes. Event deduplication does not prevent this condition.

For sensitive entities, serialize by external or local entity key. This can be achieved with queue partitions based on that key, a distributed lock with a carefully designed expiration, or a row lock within a short transaction. Another option is optimistic control: update only if the stored version is still the expected one, and retry when a conflict is detected.

Avoid keeping a transaction open while calling remote services. First reserve or read the state consistently; then make the call with an idempotency key when possible; finally record the result. If the process fails between steps, a retry must be able to distinguish a pending operation from one that has already completed.

Operations, observability, and testing before publishing

Operations, observability, and testing before publishing — DedicatedPHP visual guide

An operations dashboard should show how many events remain pending, fail repeatedly, are ignored because they are old, are rejected due to their signature, and have sequence gaps. Also measure queue age and the time from receipt to application. These signals make it possible to detect a degraded integration before the lag becomes a business problem.

Keep reprocessing mechanisms that start from the original event and an explicit version of the parser or handler. Reprocessing does not mean running blindly: limit the scope, record who requested it, and keep the same idempotency guarantees active.

Checklist

  • Send the same event multiple times, including concurrently.
  • Deliver a cancellation before the related confirmation.
  • Delay an old event until after one with a higher version.
  • Introduce gaps, unknown types, truncated payloads, and invalid signatures.
  • Simulate worker failure after creating an external effect and before marking the event as applied.
  • Verify that two workers acting on the same entity do not produce an impossible transition.
  • Check that reprocessing preserves auditing and does not duplicate effects.

A robust integration does not try to force the network to deliver in order. It designs a reliable boundary: it stores every verifiable input, applies idempotent business rules, uses a logical order when one exists, and reconciles when it cannot know the state with certainty.

Want to apply these ideas to your project?Let’s discuss your PHP platform.
View related service