HMAC signature verifier (Stripe, GitHub, Shopify, Slack)
Free webhook signature checker: paste the raw body, the signature header and your signing secret, pick Stripe, GitHub, Shopify, Slack or a custom HMAC scheme, and see if the signature matches. Runs 100% in your browser with Web Crypto; nothing is uploaded.
Paste a captured webhook request, pick the provider, and this tool recomputes the HMAC with your signing secret and tells you whether the signature matches. Everything runs 100% in your browser with the Web Crypto API — the body, the secret, and the signature never leave this page. No request is sent anywhere; pull the network cable and it still works.
Grab a real request first: create a free bin at view.webhooks.page, point the provider at it, and copy the raw body and signature header from the captured delivery.
How each provider's scheme differs
Everyone signs webhooks with HMAC, but nobody agrees on what exactly gets signed or how the result is encoded. That gap is where most verification bugs live:
- Stripe sends
Stripe-Signature: t=…,v1=…. The signed payload is not the body — it is the timestamp, a literal dot, then the raw body. Retries and secret rotation can put severalv1values in one header; any of them may match. The timestamp doubles as replay protection: Stripe's default tolerance is 5 minutes. - GitHub sends
X-Hub-Signature-256: sha256=…— plain hex HMAC-SHA256 over the raw body, nothing else mixed in. The olderX-Hub-Signatureheader is the same thing with SHA-1. There is no timestamp, so GitHub's scheme has no built-in replay protection. - Shopify sends
X-Shopify-Hmac-Sha256— HMAC-SHA256 over the raw body, but base64-encoded instead of hex. Same math as GitHub, different encoding; mixing up hex and base64 is the classic Shopify verification bug. - Slack sends
X-Slack-Signature: v0=…over a basestring ofv0:timestamp:rawBody, with the timestamp taken from theX-Slack-Request-Timestampheader. Thev0prefix versions the scheme so Slack can change it later. Slack recommends rejecting timestamps older than 5 minutes. - Custom covers the long tail: pick the algorithm and encoding, then describe the signed message with a template. Note that some senders don't HMAC the body at all — Twilio, for example, signs the full URL plus the sorted POST parameters with your auth token. Always read the provider's docs for the exact basestring.
Provider-specific walkthroughs: Stripe, GitHub, Shopify, Slack.
Why the raw body matters
An HMAC is computed over exact bytes. If you JSON.parse the body and re-serialize it, the bytes change: key order is lost, whitespace collapses, Unicode escaping and number formatting may differ. The signature was computed over the sender's bytes, not your reconstructed ones, so verification fails even though the payload is "the same JSON".
The rule is absolute: verify first, parse second. In Express that means mounting express.raw() on the webhook route instead of the global express.json(); in most other frameworks it means reading the raw request stream before any body-parsing middleware touches it. When verification fails mysteriously, a middleware that ate the raw body is the culprit nine times out of ten.
Constant-time comparison and replay windows
Two details this browser tool deliberately simplifies — don't copy them into a server:
- Constant-time comparison. A naive
===bails out at the first differing character, which in theory leaks how much of the prefix was right through response timing. Server-side, use a constant-time compare:crypto.timingSafeEqualin Node,hmac.compare_digestin Python. For interactive debugging in your own browser, plain equality is fine. - Replay windows. A valid signature proves the sender knew the secret — not that the request is fresh. An attacker who captured one signed request can replay it forever unless you check the timestamp. Stripe and Slack embed one for you (reject anything older than ~5 minutes); GitHub does not, so dedupe on the
X-GitHub-Deliveryid and keep your handler idempotent.
Common mistakes
- Verifying the parsed body. See above — verification must happen on the raw bytes, before JSON parsing.
- Signing the wrong message. Forgetting Stripe's
t + "." +prefix or Slack'sv0:ts:prefix produces a mismatch with a perfectly correct secret. - hex vs base64. Shopify is base64; Stripe, GitHub and Slack are hex. Comparing the wrong encoding never matches.
- Wrong secret. Stripe test-mode vs live-mode endpoint secrets differ, and
stripe listenprints its ownwhsec_…that is not your Dashboard endpoint's secret. - Whitespace in the pasted secret. Secrets copied from files or dashboards often carry a trailing newline. This tool trims the header value and the secret; your server code should trim the secret too.
- Header confusion on GitHub.
X-Hub-Signature(sha1) andX-Hub-Signature-256(sha256) are different headers with different algorithms — the tool auto-detects thesha1=prefix, your code should too.
Last updated
Try it in 10 seconds.
Create a free bin at view.webhooks.page — no signup, no install. Point your sender at the URL and watch requests land live. Bins expire after 48 hours.