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.
{{ p.note }}
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.
{{ headersError }}
Leave empty to just build the signed header and copy the send code below. If filled, "Send" POSTs directly from your browser — most receivers block this via CORS, so a failed send here does not mean the signature is wrong.
Paste a body, headers and the signing secret. Verification runs automatically.
{{ result.reason }}
Compared against this machine's clock. A stale timestamp does not mean the signature is wrong — an archived or replayed payload verifies but should still be rejected in production.
Enter a payload and secret, then Build or Send.
{{ sendResponse.error }}
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.