Stripe webhooks / signature verification

Stripe: “No signatures found matching the expected signature for payload”

StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Check the webhook signing secret.

The canonical Stack Overflow question for this exact message has 89,819 views and 28 answers — more traffic than almost any other Stripe integration error. Despite the message telling you to check the signing secret, that's the actual cause in a minority of cases once you've looked at it twice. Here are the 6 real causes, ranked by how often each one actually happens.

1. Wrong signing secret for THIS endpoint (most common)

Every webhook endpoint you register in the Stripe Dashboard — and every one the Stripe CLI creates with stripe listen — gets its own whsec_... value. If you have a test-mode endpoint, a live-mode endpoint, and a CLI session running locally, that's three different secrets. Copying the secret from the wrong one is the single most common cause of this error. Check: does the secret in your code match the one shown next to this specific endpoint in Dashboard → Developers → Webhooks, not just "a" webhook secret from memory or an old .env?

2. Stripe CLI secret used in a deployed environment (or vice versa)

stripe listen prints its own temporary whsec_... every time you run it — it is not the same secret as your Dashboard endpoint, and it changes on every restart unless you pin it. Code that worked against the CLI locally will fail against Dashboard-sent events with this exact error if you forgot to swap the secret for deployment, and vice versa.

3. A proxy, CDN or load balancer re-encoding the body

Signature verification needs the exact bytes Stripe sent. Some reverse proxies decompress and re-compress gzip content, normalize line endings, or re-encode the character set in transit. Any of that changes the byte sequence without changing what a human reading the JSON would notice, and the HMAC no longer matches. This is the hardest one to catch because the body looks identical when you log it as text — you have to compare raw byte length, not the printed string.

4. Middleware upstream of your handler already touched the body

A body-parser, a logging middleware that reads and re-writes the stream, or a framework that parses JSON globally before your route runs — any of these consume or transform the raw stream. Unlike the sibling error ("Webhook payload must be provided as a string or a Buffer"), this variant shows up when the framework silently hands you a string, just not the original one — for example after being decoded and re-encoded with a different line-ending convention.

5. Manually replaying a webhook payload you edited

If you copied a payload from the Dashboard's "resend" view, from a log file, or from a bug report and pasted it into a test script, any editor that changed straight quotes to curly quotes, added a trailing newline, or re-indented the JSON has changed the bytes. The signature was computed on the original bytes; your edited copy will never verify. Use Stripe CLI's actual replay (stripe events resend) instead of copy-paste for this reason.

6. Multiple server instances with different env values

Behind a load balancer, if one instance was deployed before you rotated STRIPE_WEBHOOK_SECRET and another after, roughly half your webhook traffic fails this check — intermittently, which makes it look random until you check whether every instance actually restarted after the env change.

What to check, in order

  1. Confirm the secret in your code matches the specific endpoint's secret in Dashboard right now (not cached, not from a note).
  2. Confirm you're not mixing a CLI-generated secret with a Dashboard-registered endpoint.
  3. Log the raw byte length of the body your handler receives and compare it to what Stripe's dashboard shows was sent — a mismatch points at #3 or #4.
  4. If it only fails on some requests, suspect #6 and check that every running instance has the same env value.

Want the raw Stripe-Signature header itself checked without exposing your webhook secret? The free signature validator parses the header format and timestamp — it does not need your whsec_ value, so there's nothing sensitive to paste.


This is one of 7 checks (signature verification, idempotency, raw-body middleware order, timestamp tolerance, secret key hygiene, amount trust boundary, event dedup) we run by hand against your actual repo, not a generic checklist:

Get the $39 Stripe Integration Audit — real review of your repo, 48h turnaround

Full refund if we can't access a public repo to review.