Skip to content
CodexValley

Why webhook signatures fail to verify and how to debug them

How HMAC webhook signatures work, the common reasons a correct secret still gives a different signature, and a checklist to find the cause without guessing.

Updated

Many services sign their webhooks so that your endpoint can check that a request really came from them and was not changed on the way. When the check fails even though you are sure the secret is right, the cause is nearly always that you are signing slightly different bytes, or comparing values in different formats. This guide explains how HMAC signatures work and gives a checklist to find the difference.

How an HMAC webhook signature works

The sender and you share a secret. For each request, the sender runs an HMAC (a keyed hash, defined in RFC 2104) over a message, usually the raw request body, with that secret and a hash function such as SHA-256. It sends the result in a header. Your endpoint runs the same HMAC over the bytes it received with the same secret and compares the two values. If a single byte of the message or the secret is different, the result is completely different.

So a failed check tells you only that something differs. It does not say what. Your job is to make sure four things match exactly: the secret, the message bytes, the hash function, and the way the result is written out.

The common causes

  1. The body was parsed before it was signed. Frameworks often parse JSON automatically. If you sign the parsed object turned back into JSON, spaces, key order, escaped characters or number formats can change. Always sign the raw body bytes exactly as received.
  2. Line endings or a final line break changed. Copying a body from a log, a terminal or a chat tool can turn \r\n into \n or add a line break at the end. If the Content-Length header does not match the size of your body, the body has changed.
  3. The message is more than the body. Some services sign a timestamp, an ID or the URL together with the body, often joined with a dot or a newline. Read the provider's documentation for the exact string that is signed, and build that string before computing the HMAC.
  4. Hex versus Base64. The same signature can be written as hexadecimal (64 characters for SHA-256) or Base64 (44 characters with padding). Comparing a hex value with a Base64 value never matches.
  5. A prefix or several values in the header. Headers can carry a label such as sha256= before the value, or several comma-separated parts such as a timestamp and one or more signatures. Remove the label and compare only the signature part.
  6. The wrong secret. Test and live modes, different endpoints and rotated secrets usually have different values. Check that the secret belongs to the endpoint that sent this request, and look for spaces copied at the start or end. Some providers also give a secret that has to be decoded (for example from Base64) before use, so check how yours should be read.
  7. The wrong hash function. HMAC-SHA256 and HMAC-SHA1 give different results for the same input. Use the one the provider documents.
  8. Text encoding. The HMAC is computed over bytes. If your code turns the body into text with one encoding and back into bytes with another, characters outside plain ASCII change.

A debugging checklist

  1. Log the raw request exactly: the request line, every header, a blank line and the body bytes. Do not log a parsed version.
  2. Paste it into the Webhook Debugging Helper. It splits the headers and body, shows the body size and warns if Content-Length differs.
  3. Enter the secret and choose the algorithm. The tool computes the HMAC of the body in both hex and Base64 in your browser and compares it with the signature header.
  4. If it does not match, try removing the final line break, and add the extra text the provider signs (such as a timestamp and a dot) in the prefix field.
  5. When the tool matches but your code does not, the difference is in your code: compare the body bytes your framework gives you with the ones you logged.
  6. When nothing matches, ask the provider for a test event and its expected signature, or check the secret again.

Verifying safely in your code

  • Compare signatures with a constant-time comparison function from your language or framework, not with a normal string comparison, so the time taken does not leak information.
  • If a timestamp is signed, reject requests that are too old to limit replayed requests.
  • Keep the secret in your server configuration, not in code or logs, and rotate it if it may have leaked.
  • Return a success status quickly and do slow work afterwards, so the sender does not time out and retry.

You can check any single step by hand as well: the Hash Generator shows plain SHA digests, and the Base64 Encoder & Decoder helps when a secret or signature is given in Base64. Remember that a plain hash is not an HMAC: an HMAC also mixes in the secret.

Tools for this