← All guides

Stripe webhook signature verification

A very common AI-generated bug: the handler parses JSON first, then re-stringifies it to check Stripe’s signature. Re-encoding changes the bytes, verification always fails, and the “fix” is to log a warning and carry on. Now anyone can POST a fake checkout.session.completed and unlock paid features.

1. Use the raw body

// Express
app.post('/api/stripe-webhook', express.raw({ type: 'application/json' }), handler);
// Next.js route handler
const raw = await req.text();

2. Verify, and reject on failure

let event;
try {
  event = stripe.webhooks.constructEvent(raw, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
  return res.status(400).send('Invalid signature'); // never continue
}

3. Handle retries idempotently

Stripe retries deliveries, so the same event can arrive more than once.

create table payment_events (event_id text primary key, received_at timestamptz default now());
await db.tx(async (t) => {
  const inserted = await t.result('insert into payment_events(event_id) values($1) on conflict do nothing', [event.id]);
  if (inserted.rowCount === 1) await t.none("update orders set status='paid' where id=$1", [orderId]);
});

If your own database call fails, return 500 so Stripe tries again.

4. Prove it

Use stripe trigger or the Stripe CLI’s stripe listen to send genuine test events locally.

Rather have it fixed and proven?

Our flat-fee Rescue fixes this and hands you a before/after verification report within 48h.

See pricing