← Webhook Signature Debugger

How to verify webhook signatures correctly

A webhook endpoint is a public URL that accepts requests from anyone. The signature is the only thing that says a request came from the provider. Nearly every "signature verification failed" ticket is one of three things: the body was changed before hashing, the secret is not the one for this endpoint, or the timestamp check rejected an old request.

What each provider signs

ProviderHeaderSigned stringEncoding
StripeStripe-Signature: t=…,v1=…t + "." + bodyhex, HMAC-SHA256
GitHubX-Hub-Signature-256: sha256=…bodyhex
ShopifyX-Shopify-Hmac-Sha256bodybase64
SlackX-Slack-Signature: v0=…"v0:" + timestamp + ":" + bodyhex
Standard Webhookswebhook-signature: v1,…id + "." + timestamp + "." + bodybase64; key is the base64-decoded part of whsec_…

Why the raw body matters

An HMAC changes completely if one byte changes. If your framework parses the JSON and you hash JSON.stringify(req.body), key order, whitespace and Unicode escapes may differ from what was sent. Read the unparsed bytes: express.raw({ type: "application/json" }) on that route, await request.text() in Next.js route handlers and Workers, request.get_data() in Flask. Also check for middleware that trims or re-encodes the body.

Checklist for a correct verifier

  1. Hash the raw bytes, using the provider's signed-string recipe above.
  2. Compare with a constant-time function (crypto.timingSafeEqual, hmac.compare_digest), not ==.
  3. Reject timestamps more than about five minutes from your clock, where the provider signs one.
  4. Accept any one of several signatures when a secret is being rotated.
  5. Use the secret for this exact endpoint and environment; store it outside source control.
  6. Return 2xx quickly and process asynchronously; make handlers idempotent, since providers retry.

Using the debugger

Paste headers, raw body and secret into the tool. If the signature fails but a variation matches (compact versus pretty JSON, trailing newline, CRLF, whitespace in the secret), it says which, and that points at the layer in your stack that altered the bytes.