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:
- GitHubHMAC-SHA256 over the raw body, hex, with a sha256= prefix. The older X-Hub-Signature header is SHA-1 and should not be trusted on its own.
- StripeThe timestamp, a full stop, then the body. Several v1 signatures during a rotation. The v0 value is a placeholder on test events and means nothing.
- SlackThe characters v0, the timestamp and the body joined by colons, with a five minute window you are expected to enforce yourself.
- ShopifyThe body, but base64 rather than hex, keyed with the app's client secret and not its API key.
- TwilioThe full URL with the form fields appended in alphabetical order, SHA-1, base64. Re-encoding the URL or losing the port breaks it, and a JSON body is signed through a query parameter instead.
- Standard WebhooksThe message id, the timestamp and the body joined by full stops, base64. The secret is base64 after its prefix, and it is the decoded bytes that key the HMAC, which catches almost everyone once.
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.
- 33Every file that ships, scanned for generated copy, inline styles, off-origin loads and broken asset paths ship
- 20Every provider's real scheme, including rotation, the replay window and the odd ones out verify
- 17The live page in a real browser, under the security policy it ships with e2e
- 18Layout at seven screen sizes, with the browser text enlarged, and the contrast of every line visual
- 15Redaction, the signed curl, the code samples, and reading a pasted request tools
- 11Every provider against every secret form, eight megabyte bodies, a thousand signatures in one header limits
- 12Every shape a framework leaves behind, proved on a request that really is broken that way diagnose
- 11Every control clicked in a real browser, to prove none of them is decoration ui
- 10Every number printed on this page, checked against the code behind it figures
- 10Every icon and image is the real brand asset, and the link preview belongs to this tool brand
- 8The writing itself: sentence rhythm, repeated openers, and the templates that give generated prose away humanise
What it will not do
- It will not receive webhooks for youThere is no URL to point a provider at. This page works on a request you already have, which is the point: nothing leaves your machine.
- It will not prove where a request came fromA matching signature proves whoever made it had the secret. If you signed it here a minute ago, that whoever is you.
- It will not recover a reordered bodyIf something parsed the JSON and sorted the keys, the original byte order is gone. The page says so instead of guessing.
- It will not recover hand spacingThe shapes it tries are the ones a JSON printer produces. A body that somebody indented by hand, with an array left on one line, cannot be rebuilt from a reprinted copy, and the page will say nothing matched rather than pretend.
- It will not handle asymmetric signatures yetThe Ed25519 v1a scheme in Standard Webhooks is not implemented. The HMAC v1 scheme is.
- It will not keep anythingReload the page and it is empty. There is no history, because a history of signing secrets is a liability and not a feature.
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.