Verify Stripe webhook signatures
Stripe signs every webhook with a Stripe-Signature header shaped like t=1700000000,v1=abc123.... To verify it, you HMAC-SHA256 the string `${t}.${rawBody}` with your webhook secret and compare it to v1 — normally something you only find out works (or doesn't) after deploying a route and pointing Stripe's dashboard at it. Here is how to check it before that.
1. Get a real Stripe payload to test against
Create a capture endpoint (your account's first one is permanent — a forever URL, free tier included) and point Stripe's CLI or dashboard at it instead of your app, just for this test:
curl -X POST "https://gethooklab.dev/api/v1/endpoints" \
-H "X-API-Key: hkl_your_key_here"
# → { "id": "k3v9x2m1qa", "permanent": true, ... }
stripe listen --forward-to https://gethooklab.dev/api/h/k3v9x2m1qa
stripe trigger payment_intent.succeededThe capture URL itself needs no auth — that's deliberate, Stripe has to be able to reach it. Your X-API-Key only gates reading the captures back.
2. Verify the signature in one call
No need to copy the header and body out by hand — point /api/v1/verify-signature at the capture directly:
curl -X POST "https://gethooklab.dev/api/v1/verify-signature" \
-H "X-API-Key: hkl_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"provider": "stripe",
"secret": "whsec_your_endpoint_secret",
"endpointId": "k3v9x2m1qa",
"requestIndex": 0,
"toleranceSeconds": 300
}'
# {
# "provider": "stripe",
# "source": "captured",
# "valid": true,
# "reason": "OK",
# "details": { "timestamp": 1700000000, "ageSeconds": 4 }
# }A mismatch comes back as reason: "SIGNATURE_MISMATCH" — usually the wrong secret, or a body that got re-serialized somewhere along the way (Stripe signs the exact raw bytes it sent; any JSON.parse-then-stringify round trip changes them). A stale trigger comes back as reason: "TIMESTAMP_OUT_OF_TOLERANCE" if it's more thantoleranceSeconds (default 300) old — Stripe's own replay-protection window, reproduced here on purpose.
3. Now write the handler against a known-good case
You now have a real, valid Stripe-Signature header sitting in a capture you can inspect any time — the exact shape your production code needs to parse, before you've written a route to receive it.
Also supported
The same endpoint verifies GitHub (X-Hub-Signature-256) and Shopify (X-Shopify-Hmac-Sha256) signatures — swap "provider". See the GitHub walkthrough or the full API docs.
Free tier is 1,000 captured requests a month, one endpoint, no credit card — get a key.