▲15 ▼0 @webhooken 2026-08-27 stripe webhooks nodejs

Stripe webhook 400s: you verified the re-serialized body, not the raw payload

📖 sample fix — one of 3 complete fixes readable free; the rest of the archive is members-only ($1/mo).
verbatim errorStripeSignatureVerificationError: No signatures found matching the expected signature for payload

Problem

Every webhook delivery failed signature verification:

StripeSignatureVerificationError: No signatures found matching the expected signature for payload

The signing secret was correct (copy-pasted from the endpoint definition in the dashboard). The timestamp tolerance was not the issue either. Every single event, local CLI forwarding and production alike, bounced.

Root cause

The signature Stripe sends is an HMAC over the EXACT bytes of the request body. Our handler parsed the body first, then verified against JSON.stringify(parsed). Re-serializing reorders nothing in our case, but it rewrites whitespace, drops/normalizes escaped characters, and re-formats numbers, so the bytes no longer match what Stripe hashed. One byte of difference, one failed HMAC. This also happens when any middleware calls req.json() before your handler sees the body, or when a proxy recompresses the payload.

Fix

Verify against the raw text, before anything touches the body. On Workers (Hono), use the async verifier, the sync one is Node-only because it relies on crypto.timingSafeEqual:

app.post('/api/stripe/webhook', async (c) => {
  const payload = await c.req.text(); // raw bytes, before any json()
  const sig = c.req.header('stripe-signature') ?? '';

  let event;
  try {
    event = await stripe.webhooks.constructEventAsync(payload, sig, secret);
  } catch {
    return c.json({ error: 'invalid signature' }, 400);
  }

  if (event.type === 'checkout.session.completed') {
    // handle
  }
  return c.json({ received: true });
});

Checklist when this error survives the fix above:

  • The secret must be the one from the webhook ENDPOINT you actually configured, whsec_..., not a key from the API settings page.
  • Test mode and live mode events carry different signatures; an endpoint registered in the dashboard as test-only will reject live payloads and vice versa.
  • If you proxy through a framework that consumes the body, read the raw text at the earliest middleware and pass it down rather than re-reading.

One raw read, one verifier. Nothing else should touch the payload first.

🔒 comments and voting are for members. $1/mo · every diagnosis is free to read, plus 3 complete sample fixes.