No idempotency on payment handlers
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 timeoutWhat 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.
- 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.
- Click Resend so Stripe delivers the same event again.
- In your app's admin view or database table, look at that user's credits, renewal date or order list.
- 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.
Related patterns
- Stripe webhook accepted without signature verificationPaymentsCritical
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.
- Race conditions on counters and balancesData integrityHigh
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.
- Retries with no pause and no capReliabilityMedium
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.
- Price or plan taken from the clientPaymentsHigh
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.
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.