Skip to main content
QuantLab Logo

Stripe Webhook Signature Tester

Paste a webhook body, your signing secret, and the Stripe-Signature header. We compute HMAC-SHA256 in your browser and walk you through every check — timestamp tolerance, payload format, signature compare. No data leaves the tab.

100% in-browser — secret never transmitted
Works fully offline once loaded
Catches replay-window mistakes

All cryptography runs in your browser — secret never leaves the tab

Stripe Dashboard → Developers → Webhooks → pick endpoint → Signing secret.

Awaiting verification

Paste a webhook body, secret, and Stripe-Signature header. We compute HMAC-SHA256 in your browser and walk you through every step.

How Stripe webhook signing actually works

Stripe doesn't hand you a black-box signature. The Stripe-Signature header is a comma-separated list of key=value pairs: one timestamp t= and one or more signatures keyed by scheme — currently v1=. Verification is four moving parts, and getting any one wrong sinks the whole thing.

Step 1. Split the Stripe-Signature header on commas. Pull out t=<unix_timestamp> and v1=<hex_signature>. If either is missing, reject — that request is malformed and an attacker may be probing.

Step 2. Build the signed payload as the literal string timestamp + "." + raw_body. The body must be the exact bytes Stripe sent — which means you cannot re-serialize it after JSON.parse. Most webhook failures we audit are here: Express, NestJS, or Next.js middleware mangles the body before verification.

Step 3. Compute HMAC-SHA256 of the signed payload using your endpoint's signing secret (whsec_...). This is what Web Crypto's crypto.subtle.sign("HMAC") does. Output is 32 bytes, hex-encoded to 64 characters.

Step 4. Compare your computed hex against v1 from the header — using a constant-time comparison. Never use a === b here. A string-equality short-circuit leaks signature bytes through timing.

The fifth check most teams skip. Even with a valid signature, you should reject the request if now − t > 300 seconds. Without that tolerance, an attacker who replays a signed webhook from last week — say, a successful payment — can trigger your post-payment side effects (granting access, sending product) twice. This is the entire reason the timestamp is inside the signed payload: it ties the signature to a specific moment in time.

In Stripe's own server library, this is the difference between constructEvent() succeeding and throwing a SignatureVerificationError. The library defaults the tolerance to 300 seconds. You can pass a custom value if your application has unusual clock-skew tolerance, but most teams should stay at the default.

One more nuance: Stripe's header can include multiple v1 signatures during secret rotation. When you rotate a signing secret in the Dashboard, Stripe will send both the old and new signatures for a grace period. Your verifier should accept the request if any of the v1 signatures matches your current secret. Don't assume v1=<single value>.

If you're seeing 400s in your Stripe Dashboard's webhook attempts log and you can't figure out why, drop the failing payload into this tool. The step-by-step output points directly at the broken check — almost always either timestamp tolerance or body mutation.

For deeper builds — Connect marketplaces, multi-event idempotency, dunning state machines — see our Stripe integration services page, or use the Stripe integration cost calculator to estimate scope.

Common webhook signature failures (and the fix)

Body parsed before signing

JSON.parse → JSON.stringify changes byte order, whitespace, or escaping. Solution: pass raw bytes to verification, then parse.

Wrong endpoint secret

A common one with multiple endpoints: you copied the secret from endpoint A but the webhook hit endpoint B. Each endpoint has its own whsec_.

Header lowercased

Some frameworks lowercase header names — Stripe-Signature becomes stripe-signature. HTTP is case-insensitive but your code may not be. Look up by both.

Express bodyParser swallowed it

express.json() ran before your handler. Use express.raw({type: 'application/json'}) on the webhook route only.

Clock drift on the server

If your container has bad NTP, every t= looks 'old' to you. Verify your server clock with ntpq -p before blaming Stripe.

Replay attempt outside tolerance

Stripe retries failed webhooks with the same signed payload. After 5+ minutes, the original timestamp is stale — your retry would fail the freshness check.

FAQs

Does this tool send my webhook secret to your server?

No. Every byte of computation — parsing the Stripe-Signature header, building the signed payload, computing HMAC-SHA256, comparing strings — runs in your browser using the Web Crypto API. There is no network roundtrip. You can DevTools the Network tab and confirm.

Why is my signature failing even though the secret is correct?

Nine times out of ten it is body mutation. Express's express.json() middleware reformats the JSON before your handler sees it — that breaks the signature because Stripe signed the original bytes. Use express.raw({type: 'application/json'}) for the webhook route, or set the bodyParser to false in Next.js API routes.

What is the timestamp tolerance for?

Stripe includes the unix timestamp inside the signed payload and the Stripe-Signature header. You reject requests where now − t exceeds your tolerance — typically 300 seconds. This prevents an attacker from replaying an old signed webhook against your endpoint hours or days later.

Can I use this for Stripe Connect events?

Yes. Connect webhooks use the same HMAC-SHA256 signing scheme. The only difference is the secret comes from your Connect application's webhook endpoint rather than your account's. The signature format and verification steps are identical.

Why does Stripe use v1=... instead of just a raw signature?

Versioning. If Stripe ever needs to rotate the signing algorithm — say from SHA-256 to SHA-3 — they can ship v2=... alongside v1=... during the transition. Your code should verify against v1 today and be ready to accept v2 when Stripe announces it.

Webhook signatures failing in production?

We have audited dozens of Stripe webhook integrations. Nine out of ten failures are body mutation, clock drift, or rotated secrets. A 20-minute call usually pinpoints which one — and how to harden the rest of your event handler.

Or reach us directly: (770) 652-1282 · beltz@quantlabusa.dev