“Webhook signature verification failed”

Every Stripe delivery gets a 400 back, and your error log repeats the same line.

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

  1. Read the raw request body for the webhook route and hand it to the verifier untouched.
  2. Use the signing secret of exactly this endpoint, in the same mode as the API key.
  3. 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.

Prompt for your builder
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