Tell Me When Down
How it worksWhat we checkPricing
BlogFree toolsCompareDocs
Log inGet started
All posts
Stripe webhooks you can trust

Stripe webhook signature verification failed: the raw-body fix

July 18, 2026·6 min read
Brass seal stamps on dark wood — the signature a webhook checks before it trusts a message.

"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.

This is why a webhook works perfectly against a local test and dies in production, or works in one route and not another: the difference isn't your Stripe code, it's whether a body-parsing middleware ran first. You need the raw, unparsed body specifically on the webhook route.

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 to constructEvent — don't call req.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 global express.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:

paste into Claude or ChatGPT
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.

Watch my webhookfree · no card required
more on stripe webhooks you can trust
Stripe webhooks failing silently — and how to catch itThe payment succeeds even when the webhook fails, so nothing looks broken until a customer writes in. The real retry-and-disable timeline, the raw-body trap, and how to catch it inside the window.Stripe webhook duplicate events: stop the double chargeStripe's delivery is at-least-once, so the same event can arrive twice and charge a customer twice. Here's why it happens, and how to make your handler idempotent with the event id so running it twice is safe.Stripe subscription not activating after paymentPaid but still not upgraded? The subscription's status splits it in two: stuck in 'incomplete' means the payment never finalized; active-in-Stripe-but-not-your-app means a webhook never landed. Here's how to tell which.How to test Stripe webhooks locally (and what it won't prove)The Stripe CLI brings events to your machine — stripe listen forwards them, stripe trigger fires them. Here are the commands, the signing-secret gotcha, and why passing locally doesn't prove the live endpoint works.

spot something wrong or out of date? [email protected] — we'll fix it

Tell Me When Down

Uptime and security monitoring for people who'd rather ship than babysit servers. We watch so you can sleep.

product
How it worksWhat we checkPricingDocsBlogFAQ
free toolsWebsite security scanSupabase pause checkRender sleep checkMixed content checkerSecurity headers checkCookie security checkSSL expiry check
comparevs UptimeRobotvs Better Stackvs PingdomFor indie hackers
company
StatusAbout our botSupportPrivacyTerms
© 2026 TellMeWhenDown · tellmewhendown.com