Stripe signature verification failed — check these, in order

Your endpoint gets the event, you call stripe.webhooks.constructEvent(body, sig, secret), and it throws "Webhook signature verification failed" / "No signatures found matching the expected signature for payload". The math underneath is simple: Stripe computes HMAC-SHA256(secret, timestamp + "." + raw_body) and puts it in the Stripe-Signature header as t=…,v1=…. If your check fails, exactly one of three inputs is wrong on your side: the body bytes, the secret, or the clock. Here's each, most-likely first.

1. Your framework already parsed (and re-serialized) the body

This is the cause ~80% of the time. Stripe signs the exact bytes it sent. If a body-parsing middleware runs first, what you pass to constructEvent is a re-serialization — JSON.stringify(req.body) almost never reproduces the original bytes (key order, whitespace, unicode escapes), so the HMAC can't match.

2. Wrong signing secret (they're per-endpoint)

Every webhook endpoint has its own whsec_… secret — and the Stripe CLI's stripe listen prints yet another one that belongs only to that CLI session. The classic mix-ups: verifying Dashboard-endpoint traffic with the CLI's secret (or vice versa), test-mode secret against live-mode events, or a stale env var after rotating the secret. Fix: open the exact endpoint in Dashboard → Developers → Webhooks → Reveal secret, and compare it character-for-character with what your process actually has at runtime. Pass the whole string including the whsec_ prefix — the SDK expects it.

3. Clock skew beyond the tolerance

The t= timestamp is part of the signed payload, and Stripe's SDKs reject events older (or newer) than 5 minutes by default to block replay attacks. If your server clock drifts past that, every verification fails even though the HMAC is right. Fix: sync with NTP. (You can pass a larger tolerance to constructEvent, but fix the clock instead.)

4. Something between Stripe and your code mutated the request

Less common, but real: a proxy or gateway that decompresses/recompresses, rewrites charset, or trims the body; a tunnel or middleware that touches bytes in flight. The signature check is doing its job — the bytes genuinely changed.

Isolate it in two minutes with a capture URL

The fastest way to find out which of these it is: point the same Stripe endpoint at a capture bin and let it verify independently.

$ curl https://hookden.pages.dev/new

Add the https://hookden.pages.dev/h/… URL as a (test-mode) endpoint in Stripe, set the bin's signature scheme to Stripe, paste the same whsec_… secret, and trigger an event.

Once it verifies in the bin, replay the capture to your real handler — replays resend the original bytes and headers, so the signature still verifies downstream and you can re-test your fix without triggering new Stripe events. Working on localhost? The CLI relay re-delivers captures byte-identically to a local port.

Related: Inspect Stripe webhooks without the Stripe CLI · Webhooks to localhost without a tunnel

No signup needed. Or from your terminal: curl https://hookden.pages.dev/new

← All guides · Docs · Hookden vs webhook.site