Stripe Webhook Error: 'No Signatures Found Matching the Expected Signature'
6 min read
If Stripe's constructEvent is throwing "No signatures found matching the
expected signature", it almost always means one of two things: your code read
the request body as JSON before verifying it, or you are checking against the
wrong signing secret. Both are quick to confirm and quick to fix. Neither means
Stripe is broken.
Why this happens
Stripe signs the exact bytes of the request body and puts that signature in
the stripe-signature header. To verify it, your handler has to hand Stripe the
raw body, byte for byte, plus your endpoint's signing secret. If either side
differs by even a character, the signatures cannot match.
Vibe-coded apps hit this because the agent wired up the webhook the same way it wires up every other route: it let the framework parse the body into JSON first. That is correct for normal routes and fatal for this one. By the time your code runs, the body has been re-serialised and the original bytes are gone, so the signature can never line up. The agent built a working route; it just didn't know this one route needs the raw body untouched.
How to check
- Look at what you pass to
constructEvent. If it isreq.bodyas an object, orJSON.stringify(req.body), that is the problem. It needs the raw buffer or string, exactly as received. - Check which secret you're using. Open the Stripe Dashboard, go to
Developers, Webhooks, click your endpoint, and copy the Signing secret
(
whsec_...). It is unique per endpoint. The secret shown by the Stripe CLI (stripe listen) is a differentwhsec_...and only works for the CLI. - Confirm test vs live. A test-mode endpoint's secret will never verify a live-mode event, and the reverse. Make sure the secret in your environment matches the mode the event came from.
The fix
- Give the verifier the raw body. On your webhook route only, turn off
automatic body parsing and read the raw bytes. In Next.js App Router, use
await req.text()and pass that string straight tostripe.webhooks.constructEvent(rawBody, sig, secret). In Express, mountexpress.raw({ type: 'application/json' })on the webhook path soreq.bodyis a Buffer. - Pass the header unchanged. Read
stripe-signaturefrom the request headers and pass it as the second argument. Do not trim or reformat it. - Use the endpoint's own signing secret. Put the
whsec_...from that specific dashboard endpoint into an environment variable, and read it there. If you test locally withstripe listen, use the CLI's secret locally and the dashboard's secret in production. They are not interchangeable. - Redeploy and send a test event. From the endpoint page, click Send test
webhook, or trigger a real test-mode checkout. A verified event returns a
200; the error is gone when the raw body and the right secret meet.
The trap to avoid
The tempting shortcut is to stop verifying: catch the error, skip
constructEvent, and just read req.body. Do not do this. Signature
verification is the only thing stopping a stranger from POSTing a fake
"payment succeeded" event to your public webhook URL and unlocking your product
for free. Removing the check trades a five-minute fix for an open door. Get the
raw body right instead.
Where this fits
A webhook that fails verification is a webhook that never runs, which means customers pay and nothing happens on your side. That is one of the highest-stakes gaps to close before real users arrive, and it is easy to miss because manual testing often skips the webhook entirely. The free Readiness Report checks whether your payment flow actually completes end to end, including the webhook, against your real project. If you would rather have the fixes ordered and handed to you, that is the Finishing Pass. While you are here, it is worth reading why a webhook can fail to fire at all and how to stop a retried event unlocking twice, since the same handler is where all three problems live.