Skip to content

Webhook Signature Verifier

Check an HMAC webhook signature against your own payload and secret, and find out which detail is wrong when it does not match.

Everything on this page runs in your browser. Nothing you paste is uploaded, logged, or written into the URL — only the settings are, so a configured tool can still be linked to. That matters more here than on most pages: a webhook signing secret is a live credential, and it never leaves this tab or enters the address bar.

Verification
matches

HMAC-SHA256 over <body>, hex-encoded, equals the signature you pasted.

Expected signatureHMAC-SHA256 of <body>, hex-encoded.
362cee32e8301db259bb7057ebd6d6692896e58a80217714516bdd456ba865d2
Exact string being signedByte for byte. If this is not identical to what the sender signed, nothing else matters.
{"id":"evt_5f2b","type":"invoice.paid","created":1785312000,"data":{"invoice":"in_9931","amount":2500,"currency":"usd"}}
Bytes signed
120
Body bytes
120
Secret bytes
35
Hash
HMAC-SHA256
Digest length
32 bytes
Signature candidates found in the header
1
  • passthe signature matches HMAC-SHA256 over <body>, hex-encoded
What this checked: this computes HMAC-SHA-256 or HMAC-SHA-1 over the exact string shown above and compares it with every signature-shaped token it can find in the header you pasted. When that fails it sweeps the combinations that cause almost all real mismatches — a trailing newline, CRLF endings, a re-serialised JSON body, a hex or base64-decoded secret, a timestamp or id prefix, the other hash, the other encoding — and names the one that matches. It also checks the timestamp against your replay tolerance. It does not know your provider's scheme: the three shapes offered are the common ones, and a provider that signs headers, a URL or a canonicalised body is not covered. It does not verify certificates, IP ranges or anything else about where the request came from — a valid signature proves the sender knew the secret and nothing more.
What this assumes: the secret is used as raw UTF-8 bytes unless a variant says otherwise, and the body is UTF-8. Timestamps are seconds since the epoch (a value over 1012 is treated as milliseconds), and "now" is your own browser's clock, so a machine with a wrong clock will get a wrong replay verdict here for the same reason it does in production. The comparison here is constant-time in shape but that is theatre in a browser; in your own verifier it is not theatre, and a byte-by-byte early return leaks the signature one character at a time.

The bug is nearly always the body, not the maths

HMAC is signed over bytes, and a web framework's first act is usually to turn those bytes into an object. By the time your handler runs, the body has been parsed and re-serialised, and re-serialising JSON changes it: key order can move, whitespace disappears, 1.0 becomes 1, and a non-ASCII character may come back as an escape. Every one of those changes the digest completely. That is why the sweep above tries a re-serialised body — when that is the variant that matches, the fix is to capture the raw bytes before any parser touches them, which almost every framework supports and almost no example shows.

What a matching signature does and does not prove

It proves whoever built the request had the secret. It does not prove the request is recent, which is why the timestamp is inside the signed string in the better schemes — without it, an attacker who captures one valid request can send it a thousand times. It does not prove the request is the first of its kind, so your handler still needs to be idempotent on the event id. And it says nothing about whether the event it describes actually happened: treat a webhook as a hint that something changed and fetch the current state, rather than as a statement of fact you write straight into your database.

Webhook Signature Verifier · Multigrid