new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Stripe in Production · all subjects

Webhooks and idempotency

7 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

Webhook fires twice — did I double-charge?

No, if you dedupe correctly. Stripe delivers webhooks at-least-once, not exactly-once: the same event object can arrive multiple times, and events can also arrive out of order. Your handler MUST record the event id (evt_...) and skip already-processed ids, ideally with a unique constraint in your database so concurrent retries can't both pass the check. Make the handler itself idempotent too — e.g. 'mark order paid' not 'increment paid count'. Never key dedupe on the object id (invoice, charge) alone: one invoice legitimately produces many events. Reply 2xx only after durable processing; any other status (or a timeout) tells Stripe to retry.

How long does Stripe retry a failed webhook?

Stripe retries webhook deliveries on an exponential-ish backoff over roughly three days in live mode (test mode retries for a shorter window). Delivery attempts show up in the Dashboard under Developers → Webhooks, and you can replay events manually from there. If your endpoint fails continuously for a sustained period, Stripe emails you and may automatically disable the endpoint — a disabled endpoint silently stops receiving events, which is how teams 'lose' subscription cancellations for weeks. Monitor endpoint health, alert on failure rate, and treat any gap as a reason to reconcile via the Events API (list events since last processed) rather than assuming no news is good news.

Do I need to verify webhook signatures?

Yes, always, with stripe.webhooks.constructEvent() and the endpoint's signing secret (whsec_...). An unverified endpoint is an unauthenticated POST route on your server — anyone who finds the URL can forge a checkout.session.completed and grant themselves paid access, a classic free-product exploit. Signature verification also enforces a timestamp tolerance (a few minutes) to blunt replay attacks. Gotcha: constructEvent needs the RAW request body, so in Express use express.raw({type: 'application/json'}) for the webhook route, not express.json(). In Next.js route handlers read await req.text(), never req.json(). Parse-as-JSON-then-verify always fails because re-serialization changes the bytes.

checkout.session.completed or charge.succeeded — which one grants access?

They mean different things. checkout.session.completed fires when the Checkout Session finishes — but for async payment methods (bank debits, some wallets) the payment may still be processing, and for subscriptions with a trial there may be no charge at all. charge.succeeded fires per successful charge but also fires for payments made outside Checkout. Robust pattern: handle checkout.session.completed to link the session to your user (via client_reference_id or metadata), and grant/extend access on invoice.paid (subscriptions) or payment_intent.succeeded (one-time, after confirming status isn't requires_action). Also handle checkout.session.async_payment_succeeded and ...async_payment_failed if you accept async methods.

Stripe webhook events arrived out of order — what breaks?

Anything that treats the latest received event as current state. Stripe does not guarantee ordering: you can get customer.subscription.updated (canceled) before the updated (active) from an earlier change, and end up resurrecting a canceled subscription. Never blindly overwrite local state from the event payload. Instead, when an event touches a subscription or customer, fetch the object's CURRENT state from the API (or at minimum compare the event's created timestamp against your last-processed timestamp for that object). The 'fetch current state on event' pattern makes ordering irrelevant and also protects against missed events. This is the single most common state-corruption bug in Stripe integrations.

Crash mid-charge — do I reuse the same idempotency key on retry?

Yes — that is exactly what it's for. Send the Idempotency-Key HTTP header (or the idempotencyKey option in the SDKs) on POST requests like creating a PaymentIntent, Charge, Customer, or Subscription. If your process crashes or times out after Stripe received the request, retrying with the SAME key returns the original cached response without executing the mutation again — no duplicate charge. Stripe retains keys for about 24 hours; within that window the replay is guaranteed, after it the key acts as new. Generate one key per logical operation (an order or checkout UUID), store it with the order, and reuse it across all retries of that operation. Never reuse a key for a different operation or customer. Client-side double-clicks are covered by the same server-issued key.

Duplicate webhook processing double-billed a customer — what's the recovery runbook?

Make the customer whole first, then fix the pipe. (1) Refund immediately: refund the duplicate charge with reason: 'duplicate' so it's categorized correctly and doesn't read as a dispute; email the customer proactively before they see the statement. (2) Root-cause: duplicates come from at-least-once webhook delivery hitting a non-idempotent handler, from Dashboard event resends, or from your own retry loops. Check whether your handler itself created the second charge (missing idempotency key) or merely double-provisioned access. (3) Permanent fix: a unique constraint in your database on the Stripe event id (evt_...) so concurrent deliveries can't both process, idempotent handler semantics, and Idempotency-Key headers on any charge-creation calls keyed by order id. (4) Audit: search for other duplicate charges in the same window and refund them too — one visible case usually means more.

Give your agent this brain