Stripe webhook signature verification failed: the raw-body fix

"No signatures found matching the expected signature for payload." Your webhook code is probably fine. The problem is almost always that something touched the request body before your verification did — and Stripe's signature check is exact to the byte.
Signature verification is the one part of a Stripe integration where "close enough" fails outright. Once you know what it's actually comparing, the fix is usually a one-line change to how you read the body.
What the check actually compares
When Stripe sends an event, it signs the raw request body with your endpoint's signing secret and puts the result in the Stripe-Signature header. constructEvent re-computes that signature from the body you hand it and checks the two match. The comparison is over the exact bytes Stripe sent — not the data, the bytes.
So if the body you pass in differs from what Stripe signed by even a single character, the signatures won't match and verification fails. That's the whole bug, and here's what changes those bytes:
The body parser is the usual culprit
Most web frameworks parse the request body into JSON before your handler runs. Parsing to an object and re-serializing it changes the bytes — key order, whitespace, number formatting all shift — so by the time you call constructEvent, you're verifying a re-serialized copy, not the original. Every event fails with a 400.
The fix depends on your framework, but the shape is always the same: bypass the JSON parser for this one route and read the raw body.
- Next.js App Router: read the body with
await req.text()in the route handler and pass that string straight toconstructEvent— don't callreq.json(). - Next.js Pages API: disable the built-in body parser for the route (
export const config = { api: { bodyParser: false } }) and read the raw stream. - Express: use
express.raw({ type: 'application/json' })on the webhook route, mounted before any globalexpress.json().
When it's not the body
If the raw body is definitely intact and it still fails, check these in order:
- Wrong secret. You need the endpoint's
whsec_signing secret, not your API key — and it's per-endpoint and per-mode. A test-mode secret won't verify a live event, and the CLI gives you a different secret again. - Clock skew. Verification also checks the event's timestamp against a tolerance window (five minutes by default). A badly wrong server clock makes every event look too old and fails the check.
- A proxy or logger rewriting the body somewhere in front of your app — same problem as the parser, different place.
This prompt fixes the whole thing for your specific framework:
My Stripe webhook signature verification keeps failing with "No signatures found matching the expected signature for payload." I'm using <FRAMEWORK> — tell me the exact fix. Requirements: - Explain that stripe.webhooks.constructEvent needs the EXACT raw request body bytes, and that any JSON body parser running before my handler changes those bytes and breaks verification. - Show me how to get the raw, unparsed body in my specific framework (App Router route handler, Express, Next.js Pages API, etc.), including disabling the default body parser where needed. - Confirm I'm passing the Stripe-Signature header and my webhook SIGNING secret (whsec_...), not my API key, and that I'm using the signing secret for THIS specific endpoint (test vs live, and per-endpoint). - Mention the timestamp tolerance: verification also fails if my server clock is badly skewed, since Stripe checks the event isn't older than its tolerance window. Give me the corrected handler in full.
Stripe's signature verification docs have the per-language details.
A 400 is still a silent failure
Here's the part that bites: while verification is failing, the payments still succeed. Stripe gets a 400, marks the delivery failed, and retries for three days before disabling your endpoint — but your customers paid and got nothing, and nothing on your side looks broken. A rejected signature is invisible until someone complains.
The way to catch it is to watch the endpoint from outside. The moment it starts returning 400s instead of 2xx, you want an email — inside the retry window, while you can still fix the parser and replay the missed events.
Catch a rejected webhook before your customers do.
Join Tell Me When Down free and we'll watch your Stripe webhook endpoint around the clock. The moment signature checks start failing, you get an email — in time to fix the raw-body bug and replay the events, not after Stripe disables the endpoint.
spot something wrong or out of date? [email protected] — we'll fix it