bitlvl
EN

Webhook signature verifier

Pick the provider, paste the raw request body exactly as received, the signature header and the secret. The tool recomputes the signature and shows what it expected, so you can see which part is wrong.

 

How webhook signatures work

A webhook is just an HTTP POST from someone else's server, and anyone can POST to your URL. To prove the request came from the provider, they compute an HMAC over the body with a secret only the two of you know and send it in a header. You recompute it and compare. The secret never travels; only the hash does.

Why “invalid signature” happens

  1. You hashed the parsed body, not the raw bytes. Frameworks that parse JSON before your handler runs (Express with express.json(), Rails, Django) hand you an object; re-serialising it changes whitespace and key order and the hash no longer matches. Capture the raw body.
  2. Wrong secret. Stripe has a different whsec_ per endpoint and per environment; GitHub's secret is per webhook; Slack's signing secret is not the bot token.
  3. The provider signs more than the body. Stripe signs timestamp.body; Slack signs v0:timestamp:body; Svix signs id.timestamp.body; Twilio signs the URL plus sorted form fields. The recipe for each is shown above when you pick it.
  4. Encoding. Some providers send hex, some Base64; some prefix sha256=. Compare the shapes.
  5. Proxies. A proxy or CDN that re-encodes the body (gzip, charset) or a body size limit that truncates it will break the hash.

Comparing safely

In production, compare with a constant-time function (crypto.timingSafeEqual, hmac.compare_digest, hash_equals) and check the timestamp is recent (Stripe and Slack allow five minutes) to stop replays. This page is a debugger, not a reference implementation; it uses an ordinary comparison because timing attacks against your own browser are not a concern.

Questions

Is my secret safe here?

The HMAC is computed in your browser with WebCrypto and the page makes no network requests. Even so, use a test endpoint secret and rotate anything you paste into a web page if you want to be strict.

Why does GitHub send both X-Hub-Signature and X-Hub-Signature-256?

The first is HMAC-SHA1, kept for old integrations; the second is HMAC-SHA256 and is the one to verify. Both are over the raw body with the same secret.

The expected value matches except for case. Is that a failure?

Hex is case-insensitive in every provider’s verification code, so no; this tool compares case-insensitively for hex too. Base64 is case-sensitive.

Stripe says the timestamp is too old.

Stripe’s libraries reject signatures whose t= is more than 300 seconds from the current time, to prevent replay. Replaying a captured request later will fail for that reason even though the HMAC is correct; the tool notes the age.