Stripe Webhook Handler Spec Builder

Document which Stripe events to handle and the logic for each

Builds a Stripe webhook handler specification with signature-verification order, an idempotency strategy keyed on event.id, per-event handler actions and payload fields, and the 200/400/5xx response contract. It runs free in your browser on Gera Tools, with nothing uploaded.

Last updated Source: Gera Tools

Why must I verify the signature before parsing the body?

Stripe signs the exact raw request bytes. If your framework JSON-parses and re-serializes the body first, the bytes change and constructEvent fails. Always read the raw body, verify the Stripe-Signature header, then parse.

Specify a Stripe webhook handler that survives production

Stripe sends events to your endpoint to tell you when a payment succeeded, a subscription renewed, or an invoice failed. Getting that handler right means three things that are easy to forget: verifying the signature, deduplicating retried events, and returning the correct status code. This builder turns your chosen events into a precise specification covering all three.

The processing order — why each step must come first

The order of operations in a webhook handler is not arbitrary. Each step is a gate that prevents the next one from running on bad input.

1. Read raw body bytes (before any framework JSON parsing)
2. Verify signature: stripe.webhooks.constructEvent(rawBody, sig, secret)
   → 400 on failure, stop
3. Check event.id against idempotency store
   → 200 immediately if already seen (duplicate)
4. Dispatch on event.type, run handler logic
5. Record event.id as processed
6. Return 200

Step 1 is the most commonly broken. Express, Fastify, and other frameworks will JSON-parse the request body by default. Stripe signs the exact raw bytes. If your framework has already re-serialised the JSON before you pass it to constructEvent, the signature check will fail — even with the correct signing secret. Use express.raw({ type: 'application/json' }) or the equivalent middleware before your webhook route.

Step 2 catches impersonation and replays. The Stripe-Signature header contains a timestamp and HMAC. constructEvent verifies the HMAC and rejects signatures older than 300 seconds by default, preventing replay attacks where an old valid request is sent again.

Step 3 catches duplicate delivery. Stripe’s retry logic means the same event can arrive multiple times — after a 5xx response, a timeout, or a network interruption. Store event.id in a database with a unique constraint and check it before acting. A duplicate returns 200 immediately without re-running the handler.

Per-event guidance: key fields to read

For each common event type, these are the payload fields your handler will use:

EventObjectKey fields
checkout.session.completedcheckout.Sessionid, customer, payment_intent, amount_total, metadata
payment_intent.succeededPaymentIntentid, amount, currency, customer, metadata
invoice.payment_succeededInvoicesubscription, customer, amount_paid, lines.data
invoice.payment_failedInvoicesubscription, customer, attempt_count, next_payment_attempt
customer.subscription.updatedSubscriptionid, status, current_period_end, items.data
customer.subscription.deletedSubscriptionid, customer, cancel_at_period_end

Always read from event.data.object, not from re-fetching via the API inside the handler — you already have the object, and a re-fetch inside a high-traffic handler adds latency and can hit rate limits.

Response contract and reliability

The status code is the entire contract with Stripe’s retry engine:

  • 200: Handled, or duplicate. Stripe stops retrying.
  • 400: Rejected permanently (bad signature, malformed body). Stripe does not retry.
  • 5xx or timeout: Transient error. Stripe retries with exponential backoff for up to three days.

Because events may arrive out of order and more than once, every handler must be idempotent — running it twice on the same event must produce the same final state. Keep the handler fast: acknowledge with 200 immediately and push slow work (sending emails, provisioning resources, calling third-party APIs) onto a background queue.