Stripe webhook verification: the complete guide for SaaS
Webhooks are how Stripe tells your app about payments. If verification fails silently, customers pay but never get access. Here is how to fix that.
A customer completes checkout. Stripe fires a webhook. Your server receives it, verifies the signature, provisions access, and returns 200. That is the happy path. The failure path is silent: the webhook arrives, signature verification fails, your handler returns 400, Stripe retries for 72 hours, the customer opens a support ticket, and you discover the signing secret was rotated three weeks ago.
Why signature verification is non-negotiable
Without signature verification, any POST request to your webhook URL can trigger provisioning. An attacker who discovers your endpoint can forge events, grant themselves premium access, or trigger refund logic. The signing secret proves the event came from Stripe, not from an attacker or a misconfigured test tool.
Common webhook failure patterns
The most frequent failures are: wrong signing secret (test vs live), raw body consumed before verification (middleware parsing JSON too early), endpoint not publicly reachable (firewall, preview URL, localhost tunnel expired), and handler returning 500 due to unhandled database errors. Each of these causes Stripe to retry, which delays provisioning and creates duplicate processing risk.
Preserve the raw body
Stripe signature verification requires the raw request body as a string or buffer. If your framework parses the body into JSON before your handler runs, the signature will never match. In Next.js App Router, export a config that disables body parsing for the webhook route. In Express, use express.raw() on the specific endpoint.
Match the signing secret to the environment
Stripe provides separate signing secrets for test mode and live mode webhooks. A common launch failure is deploying with the test-mode signing secret still in production environment variables. The webhook arrives, verification fails, and the event is silently dropped. PreFlight checks whether your webhook endpoint accepts events with the expected signing behavior before customers start paying.
Handle duplicate events idempotently
Stripe retries on timeout and network errors. Your handler will receive the same event multiple times. Use the event ID as an idempotency key — check if you have already processed it before writing to the database. Without this, customers can receive duplicate credits, subscriptions, or provisioning actions.
Verify the side effect, not just the 200
Returning 200 to Stripe means "I received this." It does not mean the side effect succeeded. If your handler returns 200 before the database write completes, and the write fails, you have lost the event. PreFlight's shadow checkout traces the full path: session created, webhook delivered, side effect written to the database, and verified by reading it back.
Verify continuously, not just at launch
Webhook endpoints break after launch too — when you deploy a new API version, when middleware changes, when a CDN starts buffering POST bodies, or when a signing secret is rotated without updating the environment variable. PreFlight's Sentinel runs these checks continuously so you catch webhook failures before customers report them.
