Webhook Signature Verifier

Check whether a webhook really came from Stripe, GitHub, Slack, Shopify, Paddle, Lemon Squeezy, the Telegram Bot API, Twilio, Square or HubSpot. Paste the raw body, the request headers and your signing secret — the provider's own scheme is recomputed with Web Crypto in your browser and compared against the signature it sent.

{{ PROVIDERS.length }} providers · verification is local · sending a test is optional

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. Most providers on this page solve that the same way: they hash the request body (or body plus a bit more) 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: GitHub, Shopify and Lemon Squeezy sign the body alone. Stripe signs timestamp.body, Slack signs v0:timestamp:body, and Paddle signs timestamp:body. Twilio and Square go further and sign the request URL too — Twilio's form-encoded webhooks sign url + sorted-params, its JSON webhooks sign the URL alone with the body's hash folded into a query parameter, and Square signs url + body outright — get the URL wrong (a proxy rewrote the path, or you pasted the wrong environment's URL) and the digest is wrong even with the right secret and an untouched body.

HubSpot is the other outlier: v1 and v2 are a plain SHA-256 hash of the client secret glued onto the signed string, not a keyed HMAC at all — cryptographically weaker, which is why v3 (HMAC-SHA256, plus a required timestamp) is the one to prefer for a new app. That timestamp is also the one place on this page measured in milliseconds instead of seconds — get the unit wrong and the 5-minute replay window silently accepts or rejects everything.

The Telegram Bot API entry is deliberately different again. Telegram does not hash 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: signing secrets and bot tokens are processed with the Web Crypto API in your browser and never leave it. The one exception is the "Send test webhook" button — it POSTs the built payload and signature headers directly from your browser to whatever target URL you enter, exactly like a real webhook delivery would; nothing routes through this site. Nothing is stored between sessions.

{{ toast.msg }}