How Webhook Signatures Work: HMAC, Shared Secrets, and Timing-Safe Checks
Your server receives a POST that claims to be a payment confirmation from Stripe, or a push event from GitHub. Anyone on the internet can send a POST to that URL, so the real question is: how do you know this one is genuine and not a forgery? The answer almost every provider uses is an HMAC signature, and getting the check right is more subtle than it looks. You can compute and compare HMACs by hand with the HMAC Generator, right in your browser.
Try the HMAC toolGenerate an HMAC for a message and secret key using SHA-1, SHA-256, SHA-384 or SHA-512, in your browser.HMAC: a hash with a secret mixed in
A plain hash like SHA-256 proves a message has not changed, but anyone can compute it, so it proves nothing about who sent the message. HMAC, short for hash-based message authentication code, fixes that by folding a secret key into the hashing process. The result, often written HMAC-SHA256, can only be produced by someone who knows the secret, and can only be checked by someone who knows it. So a valid HMAC proves two things at once: the message was not tampered with (integrity), and it came from a party holding the shared secret (authenticity).
| Plain hash (SHA-256) | HMAC-SHA256 | |
|---|---|---|
| Needs a key? | No | Yes, a shared secret |
| Who can produce it? | Anyone | Only someone with the secret |
| Proves integrity? | Yes | Yes |
| Proves who sent it? | No | Yes |
How a webhook uses it
When you set up a webhook, the provider gives you a signing secret that only the two of you know. From then on, every event follows the same pattern:
- The provider computes HMAC-SHA256 over the exact request body using the shared secret, producing a 32-byte tag.
- It encodes that tag as hex or Base64 and sends it in a header, for example X-Hub-Signature-256 for GitHub.
- Your server reads the raw body, computes the same HMAC with the same secret, and compares its result to the header.
- If they match, the request is genuine. If they do not, you reject it before doing anything else.
The mistake almost everyone makes: comparing wrong
Once you have computed your expected signature, you have to compare it to the one in the header, and an ordinary string comparison is a security bug here. Normal comparisons short-circuit: they return false the instant they hit a byte that does not match. That means a wrong guess that is right for the first ten bytes takes very slightly longer to reject than one that is wrong at the first byte. An attacker can measure those tiny timing differences and recover a valid signature byte by byte.
Two more details that break verification
Sign the raw bytes, not parsed JSON. If your framework parses the body into an object and you re-serialize it to verify, you will almost certainly change the whitespace or key order, and the signature will no longer match. Capture the exact raw request body before anything touches it, and HMAC that.
Guard against replays with the timestamp. A valid request captured by an attacker can be sent again later unless you stop it. This is why Stripe signs a timestamp together with the body and sends it alongside the signature: you check that the timestamp is recent and reject anything older than a few minutes, so an old but validly signed request cannot be replayed.
HMAC is not encryption
It is worth being precise about what HMAC does and does not do. It authenticates a message; it does not hide it. The webhook body travels in the clear (protected only by the TLS of the connection), and the HMAC is a tag attached to it, not an encryption of it. If you need the contents to be secret as well as trusted, that is a job for encryption on top, not for HMAC.
Test it locally
When you are debugging a signature mismatch, it helps to compute the HMAC yourself and compare it to what the provider sent. The HMAC Generator does this in your browser, so you can paste a body and secret and see the exact tag without sending either to a server. The HMAC construction itself is defined in RFC 2104, and most providers document their exact header format in their webhook guides.
Generate an HMAC nowGenerate an HMAC for a message and secret key using SHA-1, SHA-256, SHA-384 or SHA-512, in your browser.Related articles
Hashing vs Encryption: What a Hash Can and Cannot Do
Hashing is one-way and keyless; encryption is two-way and needs a key. Learn the difference, why you cannot decrypt a hash, and when to use each.
How to Read a JWT, and Why Decoding Is Not Verifying
A JWT is three Base64url parts anyone can read. Learn how to decode one, what each part means, and why decoding proves nothing.
Base64 Explained: Why Encoding Is Not Encryption
What Base64 actually does, why it makes data about a third larger, when to use it, and why it protects nothing on its own.