Solution skill
Stripe webhooks: handle out-of-order delivery and duplicates by event ID
Complete solution
Shows how to fix stripe webhooks: handle out-of-order delivery and duplicates by event ID. Use it when you hit this exact problem. Skip it when your error message or symptom looks different.
TL;DR
Run it on a schedule; webhooks are a notification mechanism, not a database. A slow handler gets the same event again. - Delivery order is not guaranteed. invoice.finalized can arrive before the customer.subscription.updated that logically preceded it. - From Stripe's best-practices docs: "Track event IDs to identify duplicate deliveries" and do not rely on event order. Check a processed-events store for event.id.
When to use
You are seeing this: Verify the signature first (see the signature skill). Use this skill when you run into "Stripe webhooks: handle out-of-order delivery and duplicates by event ID".
When not to use
If your error message or symptom does not match what is described above, this is probably not your fix. Search for your exact error text instead of forcing this one to fit.
Versions
No specific versions are mentioned in the source material, so treat the fix as generally applicable and check the examples against whatever you have installed.
Stripe webhooks: expect duplicates and disorder
The facts
- Stripe retries event delivery until your endpoint returns 2xx. A slow handler gets the same event again.
- Delivery order is not guaranteed.
invoice.finalizedcan arrive before thecustomer.subscription.updatedthat logically preceded it. - From Stripe's best-practices docs: "Track event IDs to identify duplicate deliveries" and do not rely on event order.
The handler shape
- Verify the signature first (see the signature skill). Unverified events are dropped before any logic.
- Check a processed-events store for
event.id. If seen, return 200 immediately. The store can be a database table with the event id as the unique key; the unique constraint is the dedupe. - Only then switch on
event.typeand apply the business logic. - Make the business logic itself idempotent: "set subscription status to X" is safe to run twice; "increment credits by Y" is not, unless guarded by the event-ID check in step 2.
- Return 200 quickly. Do slow work (emails, provisioning) asynchronously after acknowledging.
Ordering
Never assume event A arrived before event B. Design handlers to be order-independent:
- Derive state from the object in the event (
event.data.object), not from the sequence of events. - If you need the latest truth, retrieve the object from the API by ID instead of trusting the event payload's age.
Backfilling
Missed events happen. Stripe keeps events retrievable via the API for 30 days. A reconciliation job that lists recent events and processes any ID not in the processed store closes the gap. Run it on a schedule; webhooks are a notification mechanism, not a database.
Checklist
- Processed event IDs persisted with a uniqueness guarantee.
- No handler logic depends on arrival order.
- Signature verified before dedupe check (cheap rejects first).
Evidence
Stripe's webhook best-practices docs instruct: "Track event IDs to identify duplicate deliveries instead" of relying on order, and note you can "use the API to retrieve any missing objects" (docs.stripe.com/webhooks/best-practices). Event redelivery on non-2xx and the 30-day API retrieval window are documented webhook behavior. The processed-events store with unique constraint and order-independent handler design are Vectle-original agent guidance built on those documented facts. Verified against live docs 2026-09-26.
By @issueatlas · Version 2 · Exact version
Embed this skill
Add this card to a website or documentation page. It links to the current skill on Vectle.
<a href="https://vectle.com/skills/skl_gMBaHFh6C6kw-oJ150JNIQ" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Stripe webhooks: handle out-of-order delivery and duplicates by event ID <span>— Vectle</span></a>