Why it happens
Three usual causes. The body was parsed before verification — most frameworks parse JSON by default, and Stripe needs the raw bytes it signed. The secret belongs to a different endpoint or a different mode (test against live). Or a proxy in front of the app rewrote the body.
How to tell
- 400 on every delivery, not some — a signature problem is all or nothing.
- It began when the route or the framework changed, or when live mode was switched on.
- The secret in the environment is the signing secret of another endpoint.
The fix
- Read the raw request body for the webhook route and hand it to the verifier untouched.
- Use the signing secret of exactly this endpoint, in the same mode as the API key.
- Deploy, send a test event from Stripe, confirm 200.
Where to look
The prompt for your builder
Paste this into Lovable, Bolt, Cursor, Claude Code or whatever built the app. Replace anything in capitals. It never asks you to paste a secret into a chat; keys go into your host’s environment, by you.
My Stripe webhook route returns 400 with “signature verification failed”. Change it to read the raw request body rather than parsed JSON, verify it with stripe.webhooks.constructEvent using the STRIPE_WEBHOOK_SECRET environment variable, return 400 with a clear message if the signature header or the secret is missing, and add a test that posts a correctly signed sample event.
How Keelnest catches it
Error capture groups the repeated 400 into one fingerprint with its first-seen time and the release it began on; Revenue Guard's webhook health shows deliveries failing before payments go quiet.
Know the moment this happens.
Keelnest watches your production app every five minutes and tells you in plain English what broke — with the fix. Free for your first app.
Related
- Payments stop after a deployCheckout reaches the pay step and stalls, or orders never show as paid — and it started right after a publish.The fix
- Stripe disabled your webhook endpointPayments succeed in Stripe, but your app never marks them paid. The endpoint in Stripe's dashboard says Disabled.The fix