Stripe webhooks and idempotency

Esta publicación aún no está disponible en tu idioma. Mostrando la versión en inglés.

engineering

Stripe webhooks and idempotency

Why your invoice handler runs three times, and how to make that OK.

Alexandre Awadallak2 min read

Stripe will retry every webhook at least once. Network blips, slow processing, a bad 5xx — all of them trigger another delivery. If you double-grant credits or double-send the "Welcome" email because two copies of the same event arrived, that's your bug, not Stripe's.

Every event has a stable id#

event.id starts with evt_ and never changes across retries. Store it. On receipt, check whether you've already processed that id before doing any work.

The processed-events table#

A stripe_processed_events (event_id PRIMARY KEY, processed_at TIMESTAMPTZ) table is all you need. Insert the id inside the same transaction as the business logic. If the insert fails on the primary key, you've already handled this event — return 200 and move on.

200 fast#

Stripe's delivery timeout is short. If your handler takes more than a few seconds, Stripe will retry. Enqueue the actual work into a background job and return 200 immediately — the job processor handles retries on its own timeline.

Signature verification#

Always verify stripe-signature before trusting the payload. Stripe's SDK handles it in one line — stripe.webhooks.constructEvent(rawBody, signature, webhookSecret) — but you have to feed it the raw body, not the parsed JSON.

Test in test mode#

stripe listen --forward-to localhost:3050/api/v1/billing/webhook forwards live test events to your machine. Use it. Running stripe trigger invoice.paid once tells you more than reading the docs twice.