Payments Preflight · Check 2 of 15

Handle webhook retries and duplicate events

Stripe retries webhooks and can deliver the same event more than once. Store processed event ids so an order is fulfilled once, and return 2xx quickly.

What it is

If your endpoint does not return a 2xx, Stripe retries delivery. In live mode it retries for up to three days with exponential back off; in a sandbox it retries three times over a few hours. Endpoints can also receive the same event more than once.

Why it matters

Without dedupe, one payment can grant credits twice, send two emails or create two orders.

How to check

How to fix it

  1. Record event.id with a unique constraint before or inside the same transaction as the side effect. Skip if it already exists.
  2. Make fulfillment itself safe to run twice, for example by marking the Checkout Session as fulfilled.
  3. For your own POST calls to Stripe, send an Idempotency-Key so a retried request is not applied twice.
-- one row per Stripe event you have handled
create table stripe_events (id text primary key, type text, handled_at timestamptz default now());
-- in the handler: insert ... on conflict (id) do nothing; if no row inserted, return 200 and stop

Prompt for your AI coding tool

Paste into Cursor, Claude Code, Lovable or similar. Review the diff before you accept it.

Make my Stripe webhook handler idempotent. Add a stripe_events table keyed by event.id with a unique constraint, insert the id before running side effects, and return 200 without doing anything if the id was already processed. Make fulfillment safe to run twice for the same Checkout Session. Return 2xx before slow work. Show the migration and the diff.

Sources

All payments checks

Sponsor this page: get in touch.

Get new checks by email

New checks and real leak post-mortems, about once a month. No spam.