How to Verify Webhook Signatures
Your webhook endpoint is a public URL — anyone on the internet can send it a POST. Signature verification is how you prove a request genuinely came from the provider and not an attacker.
Why verification is non-negotiable
If your handler trusts any request that hits it, an attacker can forge events: a fake "payment succeeded" to unlock a paid feature, a fake "subscription canceled" to grief a user. Signature verification closes that hole. Every serious webhook provider signs its requests, and every production handler must check that signature.
The shared idea: HMAC
Nearly all providers use the same mechanism — HMAC (hash-based message authentication code). You and the provider share a secret. The provider computes HMAC-SHA256(secret, payload) and sends the result in a header. You compute the same hash on your side; if they match, the request is authentic and untampered.
The details — what exactly gets hashed, which header carries it, hex or base64 — differ per provider.
Stripe
Header: Stripe-Signature, formatted as t=timestamp,v1=signature. You sign the string timestamp + "." + rawBody with your whsec_… secret, hex-encoded. Stripe also lets you reject requests with an old timestamp to prevent replay attacks.
GitHub
Header: X-Hub-Signature-256, formatted as sha256=signature. You sign the raw request body with your webhook secret, hex-encoded, and compare against the part after sha256=.
Shopify
Header: X-Shopify-Hmac-Sha256. You sign the raw body with your app's secret — but Shopify expects the result base64-encoded, not hex. A common bug is comparing hex to base64.
Slack
Headers: X-Slack-Signature (formatted v0=signature) and X-Slack-Request-Timestamp. You sign the string "v0:" + timestamp + ":" + rawBody with your signing secret, hex-encoded.
The three mistakes that break verification
1. Hashing the parsed body instead of the raw body. Frameworks like Express parse JSON and re-serialize it. Re-serialized JSON is byte-different from what the provider signed, so the hash never matches. You must hash the exact raw bytes received.
2. Using a non-constant-time comparison. Comparing the signatures with == leaks timing information. Use a constant-time compare (crypto.timingSafeEqual in Node).
3. Wrong encoding. Hex vs base64 vs the wrong header. Double-check the provider's docs for the exact format.
Debugging a signature mismatch
When verification fails and you can't tell why, the fastest path is to look at the actual headers the provider sent. Capture a real webhook, read the signature header, and confirm you're hashing the right thing. Our webhook tester includes a built-in signature verifier — pick the provider, paste your secret, and it tells you whether the captured request's signature is valid, with the computed vs expected hash side by side. Secrets are checked in your browser and never leave it.
Verify a webhook signature →