Webhook Signature Verifier
Recompute a Stripe, GitHub or Slack webhook signature from the raw payload and your secret — and see exactly why it does or does not match.
—
Paste the raw body, the signing secret and the received signature, then verify. The expected signature is recomputed on this device and compared byte by byte.
—
- —
Press ? for shortcuts · Never stored — the body, the secret and the signature are credentials to this page. It keeps none of them between visits.
This page loads no third-party code — no ads, no trackers, no analytics. Verify in your Network tab.
Do it offline:$ printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"
How to use it
- Choose the provider preset. Stripe, GitHub and Slack carry their exact header grammar and signed-message law; Generic HMAC exposes the algorithm, the signature encoding, an optional prefix and a message template instead.
- Paste the raw request body exactly as delivered — from the provider dashboard's delivery view, its CLI, or your own request log. Never reformat it first: the signature covers those bytes and no others.
- Paste the signing secret (masked until you reveal it, kept only in this tab) and the received signature — the bare value and the whole header line both parse. Slack additionally takes its request timestamp beside the signature.
- Verify. The banner answers match, mismatch or malformed; the panel sets recomputed against received with the diverging tail marked; the findings strip explains timestamps, key rotation, legacy headers and encoding mix-ups.
Webhook deliveries arrive signed: the provider computes an HMAC over the exact bytes it sent, and your endpoint is expected to recompute and compare before trusting the payload. This page runs that check on demand — paste the raw body, the signing secret and the received signature, and read the recomputed value, the marked divergence and the replay-window state, on this device.
Examples
Hello, World!verdict: match
recomputed: 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
timestamp: not checkedGitHub's own documentation vector: the docs publish this secret, body and sha256= digest so implementations can check themselves, and this page reproduces it byte for byte. GitHub signatures carry no timestamp, so no window is judged.
{"id":"evt_g580_0001","object":"event","type":"payment_intent.succeeded"}verdict: match
recomputed: 211cd3f5712d0795cccaec5f049f6b46f0ccddc10cf55052d594fc422f19bd47
timestamp: skew 120 s, within toleranceStripe dot-joins the header's own t= element with the raw body before signing, so the same payload signs differently on every delivery. The v1 value is checked against that construction, the skew against the same t.
token=g580-demo&team_id=T1DC2JH3J&command=%2Fping&text=verifyverdict: match
recomputed: eae8d7c2a0c45d1b1a990c62ce2fb4cb75444f919363d61c6ffc1ed9347a5bdf
timestamp: skew 100 s, within toleranceSlack's basestring is v0, the request timestamp and the raw body, colon-joined — the timestamp travels in its own header, which is why this preset asks for it beside the signature. The v0= prefix rides the signature value itself.
{
"msg": "Hello, World!"
}verdict: mismatch
recomputed: 649eb004dcf9bbab08f91bb6cc1b8fc4f49c10b764334d7f1ab5a9aae3aae47b
timestamp: not checkedThe received signature was computed over the compact body {"msg":"Hello, World!"} — pasting a pretty-printed rendering of the same JSON changes the bytes, so recomputation disagrees and the re-serialization warning names the likely cause.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
The signature covers bytes, not meaning
A body that was parsed and re-serialized — pretty-printed, key-reordered, differently escaped — is equivalent JSON and a different byte string, and the number-one reason a correct secret still fails. This page raises a typed re-serialization warning when a mismatched payload looks reformatted, and a trailing-newline finding for the shell equivalent: echo appends the byte, printf does not.
Comparison runs in constant time
An equality check that returns at the first wrong byte tells an attacker how much of a guessed signature was right — the classic timing oracle against MAC checks. The comparison here XOR-folds every byte pair across the full length with a single exit — the shape the provider guides prescribe for servers.
The replay window judges age, not authenticity
Stripe and Slack embed a delivery timestamp so receivers can refuse replayed captures; both document a five-minute default. Skew is measured in both directions, so a receiver clock running ahead fails the same way. A stale-but-genuine delivery keeps its match verdict with the window flagged separately; tolerance zero reports skew without flagging it.
Rotation means several valid signatures at once
While a secret is being rotated, Stripe signs each delivery once per active secret and packs multiple v1= elements into one header — genuine if ANY of them recomputes, exactly how this page judges it. GitHub's rotation era instead shows up as the legacy SHA-1 header arriving beside the SHA-256 one; the legacy form still verifies and is flagged as legacy.
Common errors
This looks like a GitHub X-Hub-Signature-256 value (sha256=…) pasted under the Stripe preset — switch the provider to GitHub, or paste the Stripe-Signature header (t=…,v1=…).- Provider headers are distinctive enough to recognize, so the wrong preset is named instead of producing a meaningless mismatch. The converse pastes are caught the same way.
Stripe-Signature carries no v1= element — v1 is the live HMAC-SHA256 scheme this page checks. The header reads t=<unix seconds>,v1=<64 hex characters>.- Editors and loggers sometimes keep only the t= element or truncate the header. Without a v1 there is nothing to compare, so the grammar is spelled out instead of guessing.
The delivery's X-Slack-Request-Timestamp header is required — Slack signs `v0:<timestamp>:<raw body>`, so without it nothing can be recomputed.- Slack splits its signed material across two headers. The signature alone is unverifiable by design, so the missing half is asked for by its exact header name.
Signing secret is empty — paste the secret this endpoint was configured with (a Stripe whsec_… value, a GitHub webhook secret, or a Slack signing secret).- Each provider issues the secret in a different place, so the refusal names all three shapes. An empty secret would technically compute — and never be what the endpoint uses.
FAQ
Is my signing secret uploaded when I verify?
No. The recomputation runs in this tab and the secret is dropped with it — the body, the secret and the signature are never written to storage (only the option choices are saved, and only on this device), and the share link's state has no field for them at any version, so a link cannot carry them — verifiable in your DevTools Network tab.
The endpoint accepts this delivery — why does it fail here?
Almost always the pasted bytes differ from the delivered ones: a reformatted body, a truncated header, a test-mode secret checked against a live-mode delivery, or a Base64 signature read as hex. The findings strip names which it detected, and the diff marks where the values part ways.
Can this page receive a webhook for me?
No — receiving needs a URL on a server, and this page runs entirely on your device. Paste a delivery you already have instead: the Stripe, GitHub and Slack dashboards all expose a delivery's raw body and headers, as do their CLIs' forwarding commands.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Verify the pasted signature.
- ⌘/Ctrl + C
- Copy the expected signature when nothing is selected.
- ⌘/Ctrl + Shift + C
- Copy the expected signature regardless of selection.
- ? or ⌘/Ctrl + /
- Open the keyboard shortcuts overlay.