ApexAutomation
Apex Automation

Apex Automation free tools

Webhook Signature Verifier

It tells you what changed, not only that something did

Every other checker gives you a red cross and leaves you there. The red cross was never the hard part. The hard part is that the signature was made over the bytes the provider sent, and by the time your code sees them something has reprinted the JSON, added a newline, or swapped the line endings. This page tries each of those in turn and tells you which one your body is in.

  • Free to use
  • No account needed
  • Nothing uploaded
  • Six providers

Nothing to hand?

A whole HTTP request, a curl command, or just the headers followed by a blank line and the body. Whatever your logs give you.

What this page will read

  • A whole request from a logRequest line, headers, a blank line, then the body. The host header is used to rebuild the full URL, which Twilio needs.
  • Or a curl commandThe one your provider's dashboard gives you, or the one you built yourself. Headers and the data flag are pulled out of it.
  • Or just headers and a bodyNo request line needed. A line that is not a header stops the header block, so a body with colons in it is safe.
  • Six signing schemesStripe, GitHub, Slack, Shopify, Twilio and Standard Webhooks, each signing what it really signs rather than all being treated as plain HMAC over the body.
  • Four ways of reading a secretAs typed, with the whsec prefix removed, base64 decoded after the prefix, and hex decoded. Standard Webhooks needs the third one and almost nobody says so.
  • Several signatures at onceDuring a rotation the header carries one signature per active secret. All of them are checked and one matching is enough.
  • The clockHow old the request was when you checked it, against the window the provider actually uses, and a warning when the timestamp is ahead of now.
  • Any size of bodyNo cap. The work happens on your own machine.

What will not work. A body whose keys have been put in a different order cannot be recovered, because the order is part of what was signed, and the page says so rather than guessing. A signature made with a secret you do not have cannot be checked at all. And nothing here proves a request came from the provider if you paste a signature you made yourself.

The raw body problem, which is nearly always the problem

A webhook signature is a hash of the exact bytes the provider sent. Not of the object those bytes represent. That distinction sounds academic until you have spent an afternoon on it.

Here is the whole bug. Your framework reads the request, parses the JSON because that is what frameworks do, and hands your code a tidy object. You turn it back into a string to check the signature. The string you get back is not the string that arrived: the spacing is different, or the escaping is, and every byte after the first change is wrong. The hash of it is nothing like the one in the header.

So the useful question is never "did it match". It is "what did it used to be".

This page answers that one. It takes the body you pasted and tries it as it stands, then minified, then reindented by two and by four, then with the line endings swapped both ways, then with a trailing newline added and removed, then with a byte order mark on and off, then with escaped unicode turned back into characters. It tries your secret as typed, with the whsec prefix removed, base64 decoded, and hex decoded. If any combination matches, it says which, and what that means about where in your stack the body got changed.

What is claimed, and what is not. A match here proves the signature is right for that exact arrangement of bytes and that secret. It does not prove the request came from the provider, because you could have signed it yourself a minute ago. And where nothing matches, the page says nothing matched rather than offering a guess, because the most likely answer then is that the secret is the wrong one and no amount of trying body shapes will fix that.

Six providers, six different things being signed

Generic HMAC pages assume everyone signs the body. Almost nobody signs only the body, and the differences are where the time goes:

Each one is written out in full on the page when you check a request, so you can see the exact string that went into the hash rather than taking it on trust.

Pasting a signing secret into a web page

You should be wary of that, and the wariness is correct. A signing secret is the only thing standing between your endpoint and anyone who can guess your URL.

So here is exactly what happens to it. It goes into Web Crypto, which is built into your browser, as the key for an HMAC. It is never put in a URL, never written to storage, and never sent anywhere, because there is nowhere for it to be sent: this page has no backend. You can watch the Network tab while you use it, or pull the plug after the page has loaded and carry on working, which a test does on every run.

And when you want to show the request to somebody else, the page gives you a copy with the secrets taken out of it: bearer tokens, signing secrets, API keys for Stripe, GitHub and Slack, AWS keys, JSON web tokens, private keys, card numbers that pass a checksum, email addresses, and the secret you typed in, wherever it appears in the body.

What has actually been tested

Every figure here is a test run that has to pass before anything reaches this page. If one fails, nothing ships. A number on a page that nothing checks goes stale, so a test reads this page, pulls out every figure in it, and compares each one with the code behind it.

165automated checks, all passing
11suites, each runnable on its own
4,000pieces of junk, every one answered or refused with a reason
24deliberate faults, every one caught
6signing schemes checked against the providers' own examples
0bytes uploaded
Checked against the providers' own published examples GitHub publishes a secret, a payload and the digest they should produce. That exact vector is a test here. For the others, where no public vector exists, a request is signed and then verified back through the same path, for every provider, so a change to either half shows up immediately.
Every diagnosis is proved on a request that really is broken that way A body is signed, then deliberately reprinted, or given a newline, or given Windows line endings, or a byte order mark, and the page has to name the right cause each time. The case that cannot be recovered, keys put in a different order, is also a test: the page has to say so rather than guess.
Every combination, not just the one in the example Six providers against four ways of reading a secret is twenty eight pairs, and every one of them is signed and then verified back. An eight megabyte body is checked in a few milliseconds and diagnosed in under a second. A thousand signatures crammed into one header are all compared. A one character body, an empty body, a one character secret and a four thousand character secret all work, because the limits here are the provider's and not this page's.
Four thousand pieces of junk Half-written signature headers, empty secrets, control characters, text that is not a request at all. Every one has to come back with either a verdict or an error a person can act on. That hunt found two real crashes, both of which now return a sentence instead.

What it will not do

Questions people ask first

Why does my signature not match?

Nine times out of ten the body changed before it was checked. A framework parsed the JSON and printed it again, which changes the spacing and sometimes the escaping, and the signature was made over the original bytes. Express running express.json() before the webhook route is the classic, Next.js parsing the body is the second, and API Gateway re-encoding is the third. This page tries each of those shapes against your signature and names the one that matches, so you know where in the stack to look rather than guessing.

Is my signing secret sent anywhere?

No. The HMAC is computed by Web Crypto inside this browser tab. There is no upload endpoint and no server waiting to receive anything, and there is no storage either, so a reload empties the page. That matters more here than on most tools, because a signing secret is the one thing protecting your endpoint. You can watch the Network tab while you use it, or disconnect after the page has loaded and carry on working, which a test does on every run.

Why does Stripe send several signatures in one header?

Because a secret rotation overlaps. For up to 24 hours Stripe signs each event with every active secret, so the header carries several v1 values and your code only needs one of them to match. This page checks them all and tells you when it has seen more than one. The v0 value in the same header is a placeholder on test events rather than a real signature, so anything checking v0 is checking nothing.

What is different about Standard Webhooks secrets?

The secret looks like whsec_ followed by a base64 string, and the bytes that key the HMAC are the decoded base64, not the text. Stripe's secrets also begin with whsec_ and are used as text, so the two look identical and behave differently. That single difference produces a lot of failures that look like nothing at all, which is why this page tries both forms and tells you which one worked.

Can I use it to test my own endpoint?

Yes. Give it a body and a secret and it builds a correctly signed curl command with the timestamp set to now, so it passes the replay window. Point the URL at your own machine and you can exercise your verification code without waiting for the provider to send something. It uses your own secret, so the signature is real as far as your endpoint is concerned.

Can I share the request without sharing the secret?

Yes, and that is worth doing before you paste anything into an issue tracker. The page produces a copy of the request with the secrets removed: bearer and basic auth headers, signing secrets, Stripe and GitHub and Slack tokens, AWS keys, JSON web tokens, private key blocks, card numbers that pass a checksum, email addresses, and the secret you typed in wherever it appears in the body. It also lists what it took out, so you can check it did not miss anything.

Does it work if I only have part of the request?

Mostly. The signature header and the raw body are the two things it cannot work without. A timestamp is needed for Stripe, Slack and Standard Webhooks, and it usually travels in the header or next to it. Twilio is the awkward one: it signs the URL, so without the exact URL that was called, including its query string and port, there is nothing to check.

Which header does each provider use?

GitHub uses X-Hub-Signature-256, Stripe uses Stripe-Signature, Slack uses X-Slack-Signature with X-Slack-Request-Timestamp beside it, Shopify uses X-Shopify-Hmac-SHA256, Twilio uses X-Twilio-Signature, and Standard Webhooks uses webhook-signature with webhook-id and webhook-timestamp. You do not need to remember any of that: paste the headers and the page works out which provider sent it.

Tell us what would make this better

A signature this page gets wrong is the most useful thing anyone can send us. Redact it first with the button on the page, and the shape is enough. toolssupport@apexautomationteam.com

  • A provider whose scheme is not here yet
  • A way a body gets changed that the diagnosis does not try
  • A framework whose raw body capture is not in the code samples
  • A version of this wired into your own platform or workflow
  • Something else altogether, built to your spec

Email our team See what we build

Never send us a real signing secret. Rotate it first if you think it has been seen.