Monu Tools

How Webhook Signatures Work: HMAC, Shared Secrets, and Timing-Safe Checks

By Monu ToolsLast updated June 30, 2026

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?NoYes, a shared secret
Who can produce it?AnyoneOnly someone with the secret
Proves integrity?YesYes
Proves who sent it?NoYes

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:

  1. The provider computes HMAC-SHA256 over the exact request body using the shared secret, producing a 32-byte tag.
  2. It encodes that tag as hex or Base64 and sends it in a header, for example X-Hub-Signature-256 for GitHub.
  3. Your server reads the raw body, computes the same HMAC with the same secret, and compares its result to the header.
  4. 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