Skip to content

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.succeeded

The 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.