Skip to content

Webhook Signature Verification for Stripe and Polar

Webhook signature verification in your browser for Stripe, Polar, Standard Webhooks, Lemon Squeezy, Creem, Paddle and GitHub, with the reason a check fails.

  • Runs in your browser

Checked

01Tool

Tool

Mode
0 bytes
Paste it exactly as your server received it. A text box turns CRLF line endings into LF, so load the file to keep them.
“Name: value” lines, a JSON object or a curl command, as your server logged them. For a provider with one signature header, its value alone works too.
Seconds

Paste a raw body, its signature headers and the secret, or load an example. The check runs as you type.

Everything runs in this tab with Web Crypto. Nothing is sent or stored, and only the provider goes in the address. Use a test-mode secret, and rotate any live secret you have pasted anywhere.

02Guide

How it works

What a signature proves

Every provider here signs a webhook the same basic way: an HMAC-SHA256 over the request, keyed with a secret only you and the provider hold. Your server computes the same HMAC and compares the two in constant time. A match proves the request came from someone with the secret and that the signed bytes did not change on the way. It proves nothing about when it was sent unless a timestamp is part of what was signed.

The providers differ in the details, and each detail breaks a hand-written check: the header name, what gets signed besides the body, how the secret becomes a key, and whether the digest is hex or base64. The table further down lists all of them.

Verify the raw body, not the parsed one

The signature covers bytes. Parse the JSON and write it out again and the bytes change, even when the data does not: whitespace, key order and escaping all move. Read the body as a string, verify that string, and only then parse it.

In Nuxt, that is readRawBody from h3:

export default defineEventHandler(async (event) => {
  const body = await readRawBody(event, 'utf8')
  const signature = getHeader(event, 'stripe-signature')
})

When a check fails, the tool tries the usual changes on the body you pasted: trimmed, LF and CRLF line endings swapped, a byte order mark removed, and the JSON written compact or indented. If one of them matches, the body changed after the provider signed it, and the tool says which change it was.

Polar's two keys

Polar sends Standard Webhooks headers, but the key depends on when the secret was made. Secrets from 8 September 2026, 00:00 UTC onward are Standard Webhooks secrets: the part after whsec_ is base64, and its decoded bytes are the key. Older secrets use Polar HMAC, where the key is the whole whsec_… string as UTF-8.

Polar's SDKs from 1.0.0-alpha.19 try both keys. A plain Standard Webhooks library tries only the new one, so for an older secret, base64-encode the whole string and pass that instead. The verifier tries both and names the one that matched.

Timestamps, replays and duplicates

Stripe, Polar, Standard Webhooks and Paddle sign a timestamp, and a server should refuse a delivery whose timestamp sits too far from its own clock, even when the signature matches. That stops an attacker who captured one request from replaying it later. Keep the server's clock synced. Stripe signs each retry afresh, with a new timestamp.

Lemon Squeezy, Creem and GitHub sign the body alone, so a captured request verifies forever. Retries also mean the same event can arrive twice, as Stripe and Creem both warn. Record the event ID once you have handled it, and skip IDs you have seen.

Rotating a secret

Stripe lets you keep an old secret alive for up to 24 hours after rolling it, and sends one v1 signature per active secret in the meantime. Standard Webhooks puts several space-separated signatures in webhook-signature, and Paddle says a header can carry more than one h1 while a secret is rotated out. A check passes when any one of them matches, which is why the tool lists every signature it received and marks the one that did.

03Schemes

How each provider signs

Every provider here signs with HMAC-SHA256 and differs in the rest.

ProviderHeadersSigned contentKeyDigestTimestamp window
StripeStripe-Signature{t}.{body}The whole secret as UTF-8, whsec_ includedHex300 s
Polarwebhook-idwebhook-timestampwebhook-signature{webhook-id}.{webhook-timestamp}.{body}As Standard Webhooks, or the whole whsec_ string as UTF-8 for secrets made before 8 Sep 2026Base64±300 s
Standard Webhookswebhook-idwebhook-timestampwebhook-signature{webhook-id}.{webhook-timestamp}.{body}The base64 after whsec_, decodedBase64±300 s
Lemon SqueezyX-Signature{body}The signing secret as UTF-8HexNone
Creemcreem-signature{body}The webhook secret as UTF-8HexNone
PaddlePaddle-Signature{ts}:{body}The endpoint secret key as UTF-8, pdl_ntfset_ includedHex5 s
GitHubX-Hub-Signature-256{body}The webhook secret as UTF-8HexNone

04Questions

Questions

Why does Stripe webhook signature verification fail?
Almost always because of the secret or the body. The secret that stripe listen prints is not the one on your Dashboard endpoint, and test and live mode each have their own. The body must be the exact bytes Stripe sent, so a framework that parses the JSON and writes it out again breaks the check.
How do I verify a Polar webhook signature?
Polar follows Standard Webhooks, so the signature is an HMAC-SHA256 of the webhook-id, the webhook-timestamp and the raw body joined by dots, in base64. What changes is the key. Secrets made on or after 8 September 2026 are base64 after whsec_, and older ones use the whole whsec_ string as UTF-8. Polar's SDKs from 1.0.0-alpha.19 try both.
Does Lemon Squeezy include a timestamp in its webhook signature?
No. X-Signature is an HMAC-SHA256 hex digest of the raw body alone, keyed with the signing secret you entered when you created the webhook. Nothing in it dates the delivery, so a captured request verifies again later, and your handler has to ignore work it has already done.
How does Creem sign its webhooks?
Creem puts an HMAC-SHA256 of the raw body, keyed with your webhook secret and written in hex, in the creem-signature header. It signs no timestamp. It retries failed deliveries, so the same event can arrive twice and your handler should be idempotent.
Is it safe to paste a webhook secret into this page?
The check runs in this tab with Web Crypto, and the page makes no request with anything you type. Only the provider you pick goes in the address. Use a test-mode secret anyway, and rotate any live secret you have pasted into a website, this one included.
What is Standard Webhooks?
An open specification for signing webhooks the same way across providers. It sends webhook-id, webhook-timestamp and webhook-signature headers, signs the id, timestamp and body with HMAC-SHA256, and issues secrets as base64 with a whsec_ prefix. Polar uses it, and it ships libraries for several languages.

05Sources

Sources

Nuxter

Building a SaaS on Nuxt?

Nuxter verifies its Stripe, Polar, Lemon Squeezy and Creem webhooks with the same checks, from Stripe's list of v1 signatures and its timestamp window to both Polar keys. This page runs a browser port of that code.

See what Nuxter includesFrom $249, one-time.