Skip to content
BuildThenTestedSee packages

No idempotency on payment handlers

A customer is charged twice after a network hiccup, or a replayed webhook tops up an account again and again for a single payment. Everything between the pay button and the money arriving. Check it yourself with the steps below; our test adds what a script and a second account can find.

What it looks like

// webhook handler
case "invoice.paid":
  await db.credits.increment(userId, 100);   // runs again on every redelivery
  break;

// charge route
await stripe.paymentIntents.create({ amount, currency: "usd", customer });
// no Idempotency-Key; the frontend retries on timeout
A webhook that adds credits every time the same event arrives, and a charge call that can run twice when the browser retries. An idempotency key is a unique value that lets Stripe recognise a retry; neither call uses one.

What happens

A customer is charged twice after a network hiccup, or a replayed webhook tops up an account again and again for a single payment.

Where it shows up

Descriptive, not a verdict: builders and stacks whose defaults make this reachable, or that a public incident names. Each hub states the tool's own security features.

Check it yourself

Uses your builder's dashboard and your browser. No tools to install.

  1. In the Stripe Dashboard, open Developers, then Webhooks, select your endpoint and open a recent invoice.paid or checkout.session.completed event for a test account.
  2. Click Resend so Stripe delivers the same event again.
  3. In your app's admin view or database table, look at that user's credits, renewal date or order list.
  4. Bad result: the balance rose again or a second order appeared for one payment. Also ask the builder's chat whether payment calls pass an Idempotency-Key header; an answer of no is a bad result.

What we check

We replay each payment-related webhook several times and fire concurrent duplicate charge requests, then diff account balances, orders and Stripe charges to show exactly where duplicates land.

Fix direction

Record each processed Stripe event ID and skip events already seen, and pass an Idempotency-Key on every charge or subscription creation so a retry returns the first result instead of a second charge. Stripe documents both mechanisms and notes that it delivers some events more than once.

Mechanism documented in: Idempotent requests (verified 2026-09-21); Receive Stripe events in your webhook endpoint (verified 2026-09-21).

Covered by

Ready-to-charge check ($390, 48 hours) and Full audit ($1,900, 5 business days)

Both include a re-check after your fixes.

Compare packages

Related patterns

  • Stripe webhook accepted without signature verificationPayments

    Anyone who finds the webhook URL can post a made-up payment-succeeded event and get an order fulfilled or a plan activated for a user of their choosing, without paying.

    Critical
  • Race conditions on counters and balancesData integrity

    Users can spend the same credits twice, book the last seat twice, or redeem a coupon more times than allowed, and the stored totals stop matching what was actually given out.

    High
  • Retries with no pause and no capReliability

    One outage turns into a flood of retries from every open browser tab, which keeps the service down longer, runs up usage bills and can submit the same order or payment twice.

    Medium
  • Price or plan taken from the clientPayments

    A buyer can edit the request in their browser, pay one cent for the top plan, and your app records it as a full purchase.

    High

Questions

stripe webhook idempotency

Stripe's docs state that an endpoint might receive the same event more than once and that events can arrive out of order. The handler should store each event ID it has processed and ignore repeats. Without that, every redelivery re-runs whatever the handler does, such as adding credits or extending a plan.

duplicate webhook events double credit

Double credit happens when the handler increments a balance on each delivery instead of setting it from the event's data or checking the event ID first. You can reproduce it yourself with the Resend button in the Stripe Dashboard and watch the balance in your database.

idempotency key payments

An idempotency key is a unique string sent with a Stripe create request. Stripe saves the first result for that key and returns the same result if the request is repeated, so a retry after a timeout does not create a second charge. Generate one per purchase attempt on the server and reuse it for retries.

Back to the catalogue, or run the self-check for your stack.