Webhook signature verification fails even though the secret is right

# webhooks# api# debugging
Webhook signature verification fails even though the secret is rightAI Client Discovery

Reproduced in my own test setup — not a client's, and no real account involved: a Stripe-style...

Reproduced in my own test setup — not a client's, and no real account involved:
a Stripe-style webhook keeps failing signature verification even though the
signing secret is definitely right. Posting the reproduction because the cause
turned out to be a one-line pipeline mistake, and the error message gives you
no hint which side is wrong.

The setup: a test endpoint receives the webhook, parses the JSON body, then
verifies the HMAC signature. Secret copied straight from the dashboard, event
after event, verification fails every time.

What actually breaks it: the signature is computed over the exact bytes that
arrived, not over "the same JSON". My handler parsed the body and then
re-serialized it before verifying — pretty-printing plus sorted keys. Same
payload, same secret, different bytes, different signature. I confirmed it
with a local check: signature over the raw body verifies; signature over the
re-serialized body does not. Nothing was wrong with the secret at all.

The fix, in order:

  1. Capture the raw request body as bytes BEFORE any JSON parsing, and verify the signature against those bytes only. In most frameworks that means reading the raw body / disabling the JSON body parser on the webhook route. Only after verification passes, parse the body and process the event. If your platform parses the body for you before your code runs (some no-code webhook steps do), check whether it offers a "raw body" output — if it doesn't, verify at a step that still sees the raw payload, or move verification to a small endpoint that does.

How to tell this apart from a genuinely wrong secret: compute the HMAC
yourself over the raw bytes you actually received (log them once, exactly as
received) and compare with the signature header. If your hand-computed value
matches the header, the secret is right and something downstream is
re-writing the body — that is this bug. If it doesn't match even over the raw
bytes, then the secret (or the timestamp tolerance, if the sender enforces
one) is the problem, and no amount of re-formatting will help.

One gotcha while testing: don't "verify" by re-sending from a tool that
re-serializes the payload for you — you'll reproduce the failure and conclude
the secret is wrong. Test against the exact bytes the sender sent.