View all app connections
View all AI automations
View all chatbots
View all lead automations
View all booking automations
View all payment automations
View all workflows
View all Shopify
View all WooCommerce
View all HubSpot
View all Salesforce
View all Stripe
View all Google Sheets
View all Slack
View all dental practice
View all law firm
View all medical practice
View all real estate
View all restaurant and food
View all home services
View all construction
See all: get more leads
See all: get paid faster
See all: save admin time
See all: support customers
See all: book more appointments
See all: sell more online
See all: hire and onboard
View all AI automations
View all AI automations
View all AI automations
View all AI automations
View all AI automations
View all AI automations
View all AI automations
Payments

Stripe webhooks: retries, duplicates and order.

Why a payment event can arrive twice, late or out of order, and how to build a handler that stays correct anyway.

Reading time8 minutes
TopicPayments
Written byThe BoldBellows team

Key takeaways

  • Stripe delivers each event at least once, not exactly once, and not necessarily in order.
  • Record every event ID and ignore ones you have seen.
  • Acknowledge quickly, then fetch the current object and act on that, not on the payload.
  • Use idempotency keys for writes, and reconciliation for anything older than a day.
  • Verify signatures against the raw request body.

Every few weeks, a team discovers that a customer was charged once and received two welcome emails, two invoices or two licenses. The code looked fine. The cause is almost always the same: the webhook handler assumed Stripe would deliver each event once, in order, exactly when it happened. Stripe never promised that, and no reliable payment system can.

This guide explains what Stripe actually guarantees, the five mistakes that cause duplicate and missing actions, and a handler design that stays correct when events arrive late, twice or out of order.

What Stripe promises, and what it does not

Stripe delivers webhook events at least once. If your endpoint does not return a successful status code, Stripe retries. In live mode it keeps retrying for up to three days with increasing gaps. That is good news for reliability, and it has two consequences you have to design for:

  • An event can arrive more than once. A retry can overlap with a slow first attempt, or your server can process the event and fail to answer in time.
  • Events are not guaranteed to arrive in the order they happened. An update can reach you before the creation that preceded it.
  • Delivery can be late. After an outage on your side, a backlog arrives together, sometimes days later.
Stripeevent happensYour endpointverify, answer 2xxEvent ID ledgerseen before?Fetch current objectthen act onceFailed? Stripe retriesfor up to 3 days.Repeat, late or out of order: the ledger and the fresh fetch keep the result correct.
A safe handler verifies the signature, checks a ledger of event IDs, acknowledges quickly and then acts once on the object’s current state.

Five mistakes that cause duplicates and gaps

1. Acting on the payload instead of the current object

The event contains a snapshot from the moment it was created. By the time you process it, the object may have changed again. If you create a fulfillment from a stale ‘paid’ snapshot after the payment was refunded, you ship something that was refunded. Treat the event as a notification that something changed, and fetch the current object from the API before you act.

2. Not recognizing an event you have already seen

Every event has a unique ID. Store it the moment you accept it, in a table with a uniqueness constraint. If the insert fails because the ID exists, you have processed it, so return success and do nothing. This one habit removes most duplicate-action bugs.

3. Doing slow work before you answer

If your handler sends emails, updates a CRM and writes to a spreadsheet before it returns, a slow third party can push you past Stripe’s timeout. Stripe then retries, and you do the work again. Acknowledge fast. Verify, record the event ID, return a success code, then do the work from a queue.

4. Skipping signature verification

Anyone who finds your endpoint address can post fake events. Verify the Stripe-Signature header against the raw request body using your endpoint’s signing secret. A parsed and re-serialized body will not match, which is the commonest reason verification ‘does not work’ in frameworks that parse JSON automatically.

5. Assuming order

Do not write logic that requires customer.subscription.created to arrive before customer.subscription.updated. Write logic that looks at the subscription’s current state and decides what is needed. If a related object is not there yet, fetch it, or retry later.

A handler design that holds up

  1. Verify the signature using the raw body.
  2. Record the event ID in a ledger with a unique constraint. If it already exists, return success.
  3. Acknowledge with a 2xx response immediately.
  4. Queue the event for processing.
  5. Fetch the current state of the object the event refers to.
  6. Act idempotently: use a deterministic key for each side effect so repeating it is harmless.
  7. Alert when processing fails after retries, so a person looks at it.

Idempotency keys for the actions you take

When your handler calls Stripe to create a charge, a transfer or a refund, send an idempotency key. If the call is repeated with the same key, Stripe returns the original result instead of doing it twice. Keys can be up to 255 characters, and Stripe may remove saved keys after at least 24 hours, so a key protects you against quick retries, not against a replay next week. For longer windows, your own ledger has to remember.

Use keys that are derived from the thing being done, such as the event ID plus the action name. Random keys defeat the purpose.

Different systems, same principle

Fulfillment, CRM updates and spreadsheet rows need the same protection. Use the event ID or the object ID as the key for each downstream action, so a retry updates the same row instead of adding another.

Limits worth designing around

TopicValue to plan for
API request rateAbout 100 requests per second in live mode, 25 per second in sandboxes, for most endpoints.
Webhook retry windowUp to three days in live mode, with growing gaps between attempts.
Event history through the APIThe last 30 days. Older events must come from your own storage.
Idempotency key length255 characters maximum; saved keys can be pruned after at least 24 hours.

Check Stripe’s documentation for the current values before you rely on them. They change, and they differ by endpoint.

Recovering from an outage

If your endpoint was down, Stripe will keep retrying within its window, but you should not rely on that alone. Keep a small reconciliation job that, once a day, lists events from the API for the last day or two, compares their IDs with your ledger and processes any that are missing. Because your handler is idempotent, running it twice is harmless. The same job also catches the rare event your endpoint accepted but failed to process.

A short checklist

  • Signature verified against the raw body
  • Event ID stored with a unique constraint
  • Answer returned before slow work begins
  • Current object fetched before acting
  • Idempotency key on every write
  • Alert when processing fails
  • Daily reconciliation against the events list
  • Test mode used for every change first

Do all of this and the handler stops being fragile. It also becomes boring, which is what you want in a payment flow.

Questions

Common questions.

Short answers to what readers ask most about this topic. Ask us if yours is missing.

Does Stripe guarantee webhook order?

No. Events can arrive out of order, so design your logic around the current state of the object rather than assuming a sequence. Fetching the latest state when an event arrives is a common safe pattern.

How long does Stripe retry a failed webhook?

In live mode, for up to three days, with increasing gaps between attempts. That means a short outage on your side is usually survivable, but a long one loses events unless you reconcile afterward from the events list.

How do I prevent duplicate fulfillment?

Store each event ID in a ledger with a unique constraint, and use a deterministic idempotency key for the fulfillment action. If the same event arrives twice, the second attempt is recognized and does nothing. That is the core of a duplicate-safe automation.

Why does signature verification fail?

Usually because the body was parsed and re-serialized before verification. Signature checks need the raw, unmodified body, exactly as received. If your framework parses JSON first, capture the raw body before it does.

How far back can I list events?

The events API covers the last 30 days. Store anything you need longer, because it cannot be recovered later. A nightly job that records events is a sound way to keep your own history.

Should I process webhooks in the request?

Keep the request short: verify, record, answer, then process from a queue. Slow work inside the request risks timeouts, which cause retries and duplicates. Answering quickly and working in the background is more reliable.

What is an event ID ledger?

A table of the IDs you have already accepted, with a uniqueness constraint, so a repeat can be recognized and ignored. Before acting on an event, you try to insert its ID; if the insert fails, you have seen it before. It is simple and very effective.

Can I use polling instead of webhooks?

You can, but webhooks are faster and cheaper. Polling the events list is useful as a nightly safety net that catches anything a webhook missed. Using both gives speed and completeness.

How do I test webhooks?

Use Stripe’s test mode and its tools for sending test events, and replay the same event twice to prove your handler is idempotent. Also test an out-of-order sequence and a slow response. A webhook that survives those is ready for real use.

Need this done properly?

We build automations that handle the awkward cases: duplicates, retries, limits and exceptions. Browse fixed prices or ask for a quote.

Fixed pricesReply within 24 hours