Stripe webhook event ordering is not guaranteed. A later business event can arrive first, a delivery can be retried, and the same logical change can appear more than once.

A robust endpoint therefore treats every delivery as an independent message. It verifies the signature, records an identifier, queues the work and returns a successful response before doing slow processing.

Stripe’s webhook documentation states that live-mode events can be retried for up to three days with exponential backoff. It also warns that event delivery order is not guaranteed. Those two properties shape the entire handler.

Do not use arrival order as business order

A webhook endpoint sees requests in the order the network delivers them. That is not necessarily the order in which Stripe created the underlying objects or changed their state.

For example, a subscription workflow might generate events about a subscription, an invoice and a payment. Your endpoint could receive the payment-related event before the subscription event you expected to process first. A retry can arrive between them. Two distinct events can also share the same second-level created timestamp, so sorting only by the event’s created value does not produce a reliable total order.

The handler should load the current object state it needs rather than assuming a previous event has already populated every local field. Stripe explicitly recommends retrieving missing objects when event order matters.

That leads to a safer rule:

Treat the webhook as a notification that something changed, then reconcile from Stripe’s current object state when the workflow needs a complete view.

The event payload is still useful. It tells you which object and event type triggered the notification. It should not be the only source of truth for a multi-step state machine that assumes every earlier notification arrived first.

Separate delivery deduplication from business deduplication

There are two duplicate cases.

The first is a repeated delivery of the same Event object. The Event ID is the same, such as an ID beginning with evt_. Store that ID in a table with a uniqueness constraint. If another request presents the same ID, acknowledge it without applying the side effect again.

The second case is subtler. Stripe notes that, in some situations, two separate Event objects can represent the same underlying object change. Their Event IDs differ. To detect those, Stripe recommends considering the data object ID together with the event type.

These checks solve different problems:

  • Event ID deduplication stops a retry of one Event object from running twice.
  • Object-and-type deduplication can stop two Event objects for the same business transition from triggering a side effect twice.
  • Business idempotency protects the final operation even if the message layer misses a duplicate.

The last layer matters most for irreversible actions. If an event sends an email, provisions access or issues a refund, write a business record that represents that action. Use a unique key such as invoice-paid plus the invoice ID, rather than trusting only the webhook-delivery record.

A practical processing pipeline

A production handler can stay small at the HTTP boundary.

1. Preserve the raw body and verify the signature

Stripe signature verification uses the exact request body, the Stripe-Signature header and the endpoint secret. Parsing and re-serializing the payload before verification can change bytes and cause a valid request to fail.

Webtamine’s guide to Stripe webhook signature verification covers that boundary in detail.

2. Insert the Event ID before doing work

Open a short transaction and insert the Event ID into a delivery ledger with a unique constraint. If the insert conflicts, the handler has already accepted that Event object.

A minimal ledger might contain:

  • Event ID;
  • event type;
  • data object ID;
  • first-seen time;
  • processing status;
  • last error;
  • attempt count.

The unique Event ID is the guard. The other fields help with recovery and audits.

3. Enqueue a compact work item

Put the Event ID, event type and data object ID onto a durable queue. Do not put secrets in the message. The worker can retrieve the complete Event or current Stripe object when it needs more context.

Stripe recommends asynchronous handling so the endpoint can answer quickly. This also prevents a slow database call, email provider or downstream API from holding the webhook request open.

4. Return success promptly

After signature verification and a durable enqueue or duplicate recognition, return a 2xx response. Stripe’s documentation advises deferring complex logic and responding quickly.

A manual resend does not cancel Stripe’s automatic retry schedule. If an operator retries an event while an automatic attempt is still pending, both deliveries can reach the endpoint. The Event ID guard must handle that normal overlap.

5. Make the worker idempotent

The worker reads the current Stripe object or the required related objects, calculates the desired local state and applies it through an idempotent operation.

For a subscription, that may mean updating one row to match the current status. For a one-time side effect, it may mean inserting a record under a unique business key and performing the action only when the insert succeeds.

Marking the webhook delivery “processed” is not enough if the action and status update can be split by a crash. Where possible, commit the state transition and its idempotency marker in the same database transaction.

What to do when an expected event is missing

Do not wait indefinitely for the missing notification.

If a handler receives an event that references an unfamiliar object, retrieve that object from Stripe. If the workflow needs an invoice, customer or subscription, fetch it directly with the API and update the local model from the response.

This recovery path also helps after outages. A queue consumer can retry the job without needing Stripe to replay every related event in a perfect sequence.

Keep retries bounded and observable. Network and server failures may justify exponential backoff. Invalid data, missing permissions or a permanent configuration error need an operator-facing failure state rather than an endless loop. Webtamine’s guide to Retry-After for 429 and 503 responses explains how server guidance fits into a client retry policy.

A database pattern that survives concurrency

The deduplication insert must be atomic. A read-then-insert sequence can fail when two requests arrive together:

  1. request A checks and sees no Event ID;
  2. request B checks and sees no Event ID;
  3. both perform the side effect.

A unique database constraint changes the race. Both requests may try the insert, but only one succeeds. The loser treats the conflict as an already-accepted delivery.

The same pattern applies to business keys. If invoice inv_123 should activate an account once, use a unique record such as account-activation plus inv_123. The final guard belongs next to the side effect’s state, not only in a webhook log.

Operational checks

A webhook system needs enough records to answer four questions:

  • Did the endpoint receive this Event ID?
  • Was the request signature valid?
  • Was a work item durably queued?
  • Did the business transition complete?

Track these stages separately. A 2xx response proves acceptance, not completion. A queue message proves scheduling, not a successful side effect. A processed flag without the corresponding business state can hide a partial failure.

Avoid logging full payment objects or secrets. Store identifiers and the fields needed for diagnosis, then use Stripe’s dashboard or API for authorized inspection.