Skip to content
Integration

Consuming Stripe Webhooks: A Practical Integration Guide

How to receive, verify, and reliably process Stripe webhooks — signature verification with Stripe-Signature, fast acknowledgement, retries, idempotency, and a debugging checklist.

Published 7 min read
On this page

Stripe emits a webhook whenever something happens in your account that your code did not directly cause: a card finally clears after three retries, a subscription renews at 3am, a dispute lands, a checkout completes on a device you never see. These are the events your billing logic depends on, and they arrive asynchronously over HTTP. Getting them wrong means a customer keeps access after their card fails, or gets charged twice for one order. This guide covers receiving Stripe events correctly — verification, acknowledgement, retries, and the failure modes that bite in production.

What Stripe webhooks are

A Stripe webhook is an HTTP POST Stripe sends to a URL you register, carrying an Event object. Every event has a stable id (prefixed evt_), a type such as invoice.payment_failed, and a data.object holding the resource the event is about — an invoice, a subscription, a checkout session. Because payment state is authoritative on Stripe’s side and only eventually consistent on yours, webhooks are how your application learns the truth about money after the fact.

Common use cases

  • Provisioning after payment — grant access on checkout.session.completed or invoice.paid rather than trusting a client-side redirect that a user can close.
  • Dunning and failed payments — react to invoice.payment_failed to email the customer, pause a subscription, or start a retry sequence.
  • Subscription lifecycle — keep entitlements in sync from customer.subscription.updated, customer.subscription.deleted, and trial-ending events.
  • Reconciliation — record charge.refunded, disputes, and payouts against your own ledger.

Configuring the webhook in Stripe

You register endpoints in the Stripe Dashboard under Developers → Webhooks, or with the API. At a high level you supply the destination URL, select the event types you care about (subscribe narrowly — do not send every event to every endpoint), and Stripe issues that endpoint a signing secret (prefixed whsec_) used to verify deliveries. Test mode and live mode have separate endpoints and separate secrets.

Exact dashboard labels and options change over time, so treat the flow above as stable and confirm current specifics against Stripe’s webhook documentation. Store the signing secret as a secret, not in source, and remember that a test-mode secret will never verify a live event.

Stripe waits only a short time for a response and treats a slow endpoint as a failure, so the handler’s job is to acknowledge quickly and do the real work elsewhere:

  1. Read the raw request body before any JSON parsing (you need the exact bytes for signature verification).
  2. Verify the signature.
  3. Persist or enqueue the raw event, keyed by its evt_ id.
  4. Return 2xx immediately.
  5. Process asynchronously — provisioning, emails, ledger writes — off the request path.

Filter by event type at the edge too. If an endpoint only cares about invoice events, ignore the rest early. HookWatch endpoints support an event-type allowlist so unrelated types never reach your handler in the first place.

Verifying authenticity

Stripe signs every delivery with the Stripe-Signature header. It contains a timestamp and one or more signatures, formatted roughly like this:

Stripe-Signature: t=1753228800,v1=6ffbb59b2300aabbccddeeff00112233445566778899aabbccddeeff0011223344

The v1 value is an HMAC-SHA256, computed with your endpoint’s signing secret, over the string timestamp + "." + raw_request_body. To verify: recompute the HMAC over the concatenation of the t value, a literal ., and the raw body bytes, then compare in constant time against v1. Also reject deliveries whose timestamp is outside a tolerance window (Stripe’s libraries default to five minutes) to blunt replay attacks.

The single most common mistake is verifying a parsed-and-re-serialised body instead of the raw bytes — any reordering or whitespace change breaks the HMAC. Keep the raw body. The mechanics, with constant-time comparison and worked code, are covered in how to verify webhook signatures.

A note on gateways: HookWatch’s configurable inbound verification covers plain HMAC-over-body schemes (header name, algorithm, encoding, optional prefix). Stripe’s scheme signs a timestamped payload and packs the result into a composite Stripe-Signature header, so verify it in your own handler with Stripe’s library or the raw-body HMAC pattern above rather than expecting a generic gateway check to replace it.

Common failure scenarios

  • Signature failures from a parsed body — a middleware that JSON-parses before your verifier runs, or a proxy that rewrites the body, produces a mismatch even though the secret is correct.
  • Wrong-mode secret — verifying live events with a test-mode whsec_, common right after go-live.
  • Missed events after an endpoint is disabled — Stripe can disable an endpoint that fails persistently; deliveries stop and gaps appear in your data. Watch for this and re-enable deliberately.
  • Timeouts under load — synchronous provisioning or a slow downstream call pushes the response past Stripe’s window, so a delivered event is recorded as failed and retried.
  • Duplicate side effects — a retry after a timeout that actually succeeded runs your handler twice.

Testing workflow

The Stripe CLI is the fastest local loop. stripe listen --forward-to forwards live account events (or CLI-triggered ones) to a local URL, and stripe trigger invoice.payment_failed fabricates a specific event on demand — ideal for exercising one branch without real payment flows. The CLI prints its own signing secret for the forwarded session; use that secret locally.

To test the real path — including your public TLS, proxy, and framework body handling — expose your local handler with a tunnel and point a real Stripe endpoint at it. See testing webhooks locally; hookwatch tunnel streams captured deliveries straight to your local process so Stripe can reach your laptop through a HookWatch endpoint.

Retry behaviour

Stripe retries failed deliveries automatically, with exponential backoff, over a limited window. The exact schedule and window have changed before, so rely on the behaviour rather than specific intervals and confirm the current policy in Stripe’s docs. What matters for your handler:

  • A 2xx is the only signal Stripe reads as success. A 500, a timeout, or a connection error all trigger retries.
  • Retries mean the same event will arrive more than once during incidents — plan for it rather than hoping it does not happen.
  • Return 4xx only for requests you genuinely reject; Stripe treats client errors as permanent and stops retrying, so a 400 from a transient bug loses the event.

Your own retry posture matters as much as Stripe’s — see backoff, jitter and attempt limits.

Idempotency

Because retries and at-least-once delivery are normal, dedupe on the event id (evt_...), which is stable across every retry of the same event. Record processed ids and make the processing itself idempotent — provisioning the same subscription twice should be a no-op, not a second charge. A dedupe that already saw an event should still return 2xx so Stripe stops retrying. Full patterns are in webhook idempotency.

Debugging checklist

  1. Confirm the event was sent from the Stripe Dashboard’s event log, to the URL and mode (test vs live) you expect.
  2. Check the response Stripe recorded — a status code, a timeout, or a connection error each point somewhere different.
  3. Verify your handler reads the raw body before any parser touches it.
  4. Confirm the signing secret matches the endpoint and the mode of the event.
  5. Confirm the timestamp tolerance check is not failing on clock skew.
  6. Make sure the handler returns 2xx well inside Stripe’s timeout, with heavy work deferred.
  7. Match your application logs to the delivery by evt_ id.
  8. Verify duplicate events are deduped and still answered with 2xx.
  9. Check the endpoint is still enabled in Stripe and not auto-disabled after failures.

For a Stripe-specific walkthrough, see Stripe webhooks, why a Stripe webhook is not working, and Stripe signature verification.

How HookWatch helps

Point a HookWatch endpoint at Stripe and it records every delivery in full — request headers and body, response status and body, and a numbered attempt history — so a failed invoice.payment_failed still shows its exact payload and your handler’s error when you investigate later. When you have deployed a fix, replay the exact captured event (individually or in bulk) without asking Stripe to resend, pause an endpoint while you work and resume it after, and let the event-type allowlist keep noise out. Repeated failures on the same endpoint are grouped into an incident so a Stripe outage on your side is one alert, not a hundred.

Get started

See what happened to every webhook.

HookWatch keeps the request, the response, and every attempt for each delivery — so the debugging, retry, and replay steps in this article are a matter of reading, not reconstructing.