Stripe Webhook Signature Verification Failed: Causes & Fixes
Why Stripe's webhook signature verification fails — parsed instead of raw body, wrong signing secret, timestamp tolerance, proxies — and how to fix each, with code.
Ozer
Developer & Founder of HookSense
Few errors waste more time than "No signatures found matching the expected signature for payload". Your code looks correct, the secret looks right, and yet every Stripe webhook is rejected. The good news: signature verification fails for a small, well-understood set of reasons. This guide walks through each one in the order you should check them.
How Stripe signatures actually work
Stripe sends a Stripe-Signature header shaped like t=1614264600,v1=5257a8.... The t is a timestamp; v1 is an HMAC-SHA256 signature. Stripe computes that signature over a specific string: the timestamp, a literal dot, and the exact raw bytes of the request body. Your SDK re-computes the same value using your endpoint's signing secret and compares the two.
The critical phrase is exact raw bytes. If the body your code hashes differs from what Stripe hashed by even one byte, the result won't match. That single fact explains the overwhelming majority of failures.
Cause 1: You verified the parsed body, not the raw body
This is the number-one cause. Frameworks like Express parse JSON automatically, so by the time your handler runs, req.body is a JavaScript object. Re-serializing it with JSON.stringify reorders keys and changes whitespace — different bytes, different signature.
// WRONG — express.json() already parsed the body
app.post("/webhooks", express.json(), (req, res) => {
stripe.webhooks.constructEvent(JSON.stringify(req.body), sig, secret); // fails
});
// CORRECT — verify the raw Buffer before parsing
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const event = stripe.webhooks.constructEvent(req.body, sig, secret); // passes
});
The same trap exists everywhere: Next.js API routes need bodyParser: false; many serverless platforms pre-parse the body and you must reach for the raw event instead. If you take one thing from this article: verify before you parse.
Cause 2: Wrong signing secret
Every endpoint has its own secret (whsec_...), and they're easy to mix up:
- The dashboard endpoint secret and the Stripe CLI secret (printed when you run
stripe listen) are different values. Local tests use the CLI secret; deployed endpoints use the dashboard secret. - Test mode and live mode have separate secrets. A live webhook checked against a test secret fails.
- If you have several endpoints, make sure the secret you load matches the exact endpoint that received this event.
Print the first few characters of the secret your code loaded (never the whole thing) and confirm it matches the endpoint in the dashboard.
Cause 3: Expired timestamp on replayed or delayed events
Stripe's library rejects payloads whose timestamp is older than its default 5-minute tolerance, as replay-attack protection. You'll hit this when you replay an old captured webhook, when your server clock has drifted, or when a slow queue delays verification. If clock drift is the suspect, sync with NTP. If you're deliberately replaying for tests, re-send a fresh event or re-sign with your test secret rather than fighting the tolerance window.
Cause 4: A proxy or platform rewrote the body
Sometimes your code is correct and the bytes still changed in transit. A WAF, CDN, or proxy that re-encodes JSON, applies compression, or normalizes charset will alter the body Stripe signed. Serverless platforms that base64-encode or pre-parse the request are common culprits.
To prove whether the body was mangled, capture the request as it actually arrives. Point your Stripe endpoint (or a copy of the event) at a HookSense URL, then compare the raw body and byte length there against what Stripe shows in its dashboard. If they differ, the problem is infrastructure, not your handler.
A debugging checklist
- Confirm you're verifying the raw body, before any JSON middleware.
- Confirm the signing secret matches the exact endpoint (and mode) that received the event.
- Rule out an expired timestamp — replays, clock drift, slow processing.
- Capture the request on an inspector and check whether a proxy altered the bytes.
- Check the
Stripe-Signatureheader is present and not truncated by your logging.
HookSense captures the exact raw body Stripe sends, shows its byte length, and lets you replay it to your handler while you experiment with the fix — so you can tell a body-mangling bug from a wrong secret in seconds. Create a free endpoint and point a test Stripe event at it.
Related posts
Related terms
Try HookSense Free
Inspect, debug, and replay webhooks in real-time. No credit card required.
Get Started Free