Webhook Signature Verifier

Check whether a webhook really came from Stripe, GitHub, Slack, Shopify, Paddle, Lemon Squeezy or the Telegram Bot API. Paste the raw body, the request headers and your signing secret — the HMAC-SHA256 is recomputed with Web Crypto in your browser and compared against the signature the provider sent.

{{ PROVIDERS.length }} providers · local only

Provider

{{ p.label }} — signature scheme

Header{{ p.headerName }}
Format{{ p.headerFormat }}
Algorithm{{ p.algo }}
Signed string{{ p.signedDesc }}
Encoding{{ p.encoding }}
Secret{{ p.secretDesc }}
Replay window{{ p.toleranceDefault ? p.toleranceDefault + 's recommended' : 'not part of the scheme' }}

{{ p.note }}

Request

Raw request body

Must be the exact bytes your server received. Re-serialising parsed JSON changes whitespace and key order, and the signature will no longer match.

This exact text is sent as the raw request body — the signature is computed over these bytes.

Result

Signature valid Signature invalid Awaiting input Send failed Sent · {{ sendResponse.status }} Request built Not built
Server-side verification for {{ activeProvider.label }} in {{ activeLang.label }}. Build and send a signed {{ activeProvider.label }} test event in {{ activeLang.label }}.
{{ exportedCode }}

About the Webhook Signature Verifier

A webhook endpoint is a public URL, so anyone can POST to it. Every provider on this page solves that the same way: they HMAC the request body with a shared secret and put the digest in a header. Your server recomputes the digest and rejects anything that does not match. This tool does that recomputation in the browser so you can tell a genuine delivery from a forged one — and, more often, work out why a delivery you know is genuine keeps failing verification.

The usual culprit is the body. Frameworks that parse JSON before your handler runs hand you an object, and re-serialising it changes whitespace or key order, which changes the digest. You need the raw bytes: express.raw(), request.get_data(), file_get_contents('php://input'). The second culprit is the signed string: only GitHub, Shopify and Lemon Squeezy sign the body alone. Stripe signs timestamp.body, Slack signs v0:timestamp:body, and Paddle signs timestamp:body — sign the wrong thing and the digest is wrong even with the right secret.

The Telegram Bot API entry is deliberately different. Telegram does not HMAC anything: setWebhook takes a secret_token and echoes it back verbatim in X-Telegram-Bot-Api-Secret-Token, so verification is a constant-time string comparison. It is listed here because it is the same job — proving a webhook is authentic — not because it shares the algorithm.

Privacy: 100% client-side. Signing secrets, bot tokens and payloads are processed with the Web Crypto API in your browser and are never sent anywhere. Nothing is stored between sessions.

{{ toast.msg }}