StripeRunbook 0/4 done
StripeSignatureVerificationError

Stripe Error StripeSignatureVerificationError: Webhook signature verification failed

Error✕ Not retryable · fix the payloadHTTP 400 Bad RequestAPI Gateway
TL;DR · 10-second read

The HMAC of the body you passed does not match the Stripe-Signature header. Pass the raw, unparsed request body and the whsec_ secret of this exact endpoint.

Identify: does your log match?

What you see · exact log / response
StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe?
 If a webhook request is being forwarded by a third-party tool, ensure that the exact request body, including JSON formatting and new line style, is preserved.

Learn more about webhook signing and explore webhook integration examples for various frameworks at https://docs.stripe.com/webhooks/signature
    at validateComputedSignature (/app/node_modules/stripe/cjs/Webhooks.js:149:19)
    at Object.verifyHeaderAsync (/app/node_modules/stripe/cjs/Webhooks.js:98:20)
  1. High

    Body parsed as JSON before verify

    express.json() or a framework re-serializes the body, changing whitespace and key order. The signature was computed over the original bytes.

  2. High

    Wrong signing secret

    Each endpoint, and each stripe listen session, has its own whsec_ secret. Test vs live and CLI vs Dashboard secrets differ.

  3. Low

    Timestamp outside tolerance

    The t= timestamp is older than the 300-second default tolerance, e.g. replayed events or a skewed server clock.

Fix

Use express.raw on the webhook route only

✕ Broken · Node.js
app.use(express.json());
app.post('/webhooks/stripe', (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
});
✓ Fixed · Node.js
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  res.json({ received: true });
});
app.use(express.json());

express.raw keeps req.body as a Buffer of the exact bytes Stripe signed. Register it before any global JSON parser.

Verify: reproduce it on purpose

Reproduce locally · curl
$ curl -s -X POST http://localhost:3000/api/webhooks/stripe -H "Content-Type: application/json" -H "Stripe-Signature: t=1700000000,v1=deadbeef" -d '{"id":"evt_test","type":"payment_intent.succeeded"}'

Run it against a dev environment to trigger StripeSignatureVerificationError on purpose and test your error handler.

Prevent

  • Store whsec_ per environment; log only its last 4 chars.
  • Return 2xx fast and process events in a queue.
  • Make handlers idempotent on event.id; Stripe retries for up to 3 days.

FAQ

Why does verification pass with stripe listen but fail in production?

stripe listen prints its own whsec_ secret. Production must use the secret from the Dashboard endpoint (Developers > Webhooks > Signing secret).

Does a proxy or API gateway break the signature?

Yes, if it re-encodes, decompresses or pretty-prints the JSON body. Forward the body byte-for-byte.

Can I skip verification in development?

You can, but you then accept forged events. Use stripe listen --forward-to and its secret instead.

Related errors

Last verified 2026-10-23 · Source: Stripe OpenAPI spec + decline codes + webhook docs · Official docs ↗ · ~9,900 searches/mo