Tell Me When Down
How it worksWhat we checkPricing
Security reportsBlogFree toolsCompareDocs
Log inGet started
All postsStripe webhooks you can trust

Stripe webhook signature verification failed: every error and its fix

July 18, 2026·8 min read·updated August 19, 2026
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.

Stripe webhook signature errors: what each message means

If you got here by pasting an error, start with the exact message. constructEventthrows seven of them, and they are not interchangeable — one of them isn't about the body at all. They are listed in the order the library checks them.

No signatures found matching the expected signature for payload.

the signature comparison

Cause
The bytes you passed in are not the bytes Stripe signed. Nearly always a JSON body parser ran first, parsed the body to an object and re-serialised it — which changes key order, whitespace and number formatting, and so changes the signature.
Fix
Read the raw body on this route and pass that. await req.text() in an App Router handler, express.raw() mounted before express.json()in Express. If the body is definitely raw, check you are using this endpoint's whsec_ secret for this mode — and check the secret for a stray newline, which the SDK will warn about in the same message.

Webhook payload must be provided as a string or a Buffer instance representing the _raw_ request body.

the signature comparison, when the payload is an object

Cause
The same bug as above, caught one step earlier: you handed constructEvent a parsed JavaScript object instead of the raw body. Stripe can tell, so it says so specifically rather than just reporting a mismatch.
Fix
Stop calling req.json() or req.body on the webhook route. Once the body has been parsed the original signed bytes are gone — they cannot be reconstructed by re-serialising, which is the whole reason this is a distinct error.

Timestamp outside the tolerance zone

the timestamp check, after the signature already matched

Cause
The signature was fine. The event is simply older than the tolerance window — five minutes by default. Either your server clock has drifted, or you are replaying a captured request long after it was sent.
Fix
Sync the clock with NTP; on a container host that usually means the host, not the container. This one is worth recognising on sight, because it is the only error in this list that is not about the body — chasing the parser here wastes an afternoon.

No webhook payload was provided.

the first check, before anything is parsed

Cause
The payload argument was empty or undefined. Usually the raw-body reader returned nothing: the stream was already consumed by middleware, or the framework's body parser was disabled without anything put in its place.
Fix
Log the body length before verifying. Zero means the problem is upstream of Stripe entirely, and no amount of signature debugging will find it.

No stripe-signature header value was provided.

the header check

Cause
The Stripe-Signature header never reached your handler. Either something in front of the app strips unknown headers, or the request did not come from Stripe at all — a health check, a scanner, or your own test curl.
Fix
Check the proxy or gateway first. If the header is absent on real Stripe deliveries, the event never had a chance to verify, and the endpoint is effectively unprotected against the requests that do carry a body.

Unable to extract timestamp and signatures from header

parsing the header

Cause
The header arrived but is malformed — it should look like t=1690000000,v1=abc…. Something rewrote it, truncated it, or a testing tool forged a header of its own shape.
Fix
Log the header verbatim and compare it against a real delivery in the Stripe dashboard. If they differ, the bug is in whatever sits between Stripe and your handler.

No signatures found with expected scheme

parsing the header

Cause
The header parsed, but contains no v1 signature — the scheme the library expects. Typically a hand-built header in a test, or a replay tool that copied the timestamp and dropped the rest.
Fix
Generate test events with the Stripe CLI rather than hand-rolling the header. The CLI signs them properly, with its own signing secret.

The first two are the same bug wearing different clothes, and they cover the large majority of real cases. The rest of this post is why.

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 checkSecurity reportsPricingDocsBlogFAQ
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 botContactPrivacyTerms
© 2026 TellMeWhenDown · tellmewhendown.com