SkillByAIOpen interactive version →

Lesson 8 / 25

Verifying Webhook Signatures

The raw body and the endpoint secret.

Signatures prove the event came from Stripe

Anyone can POST to a public URL, so every webhook must be verified. Stripe signs each request and puts the signature and a timestamp in the Stripe-Signature header. Your endpoint has a signing secret (whsec_...). The library's stripe.webhooks.constructEvent(rawBody, signature, secret) recomputes the signature and throws if it does not match or the timestamp is too old, which also guards against replayed requests. The check needs the exact raw request body: if a JSON body parser has already parsed and re-serialised it, verification fails.

An Express webhook endpoint

Use the raw body for this route only.

import express from 'express';
const app = express();

app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, signature, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook signature verification failed: ${err.message}`);
  }
  await handleEvent(event);      // keep this quick, or enqueue it
  res.json({ received: true });  // a 2xx tells Stripe the delivery succeeded
});

// register JSON parsing for other routes after the webhook route
app.use(express.json());

Each endpoint has its own secret

The secret printed by the Stripe CLI, the test-mode endpoint secret and the live-mode endpoint secret are all different. A signature error after deploying usually means the wrong secret or a parsed body.

Quick check: What does constructEvent need in order to verify a webhook?

  • Only the event ID
  • The parsed JSON body and the publishable key
  • The raw request body, the Stripe-Signature header and the endpoint secret
  • The secret API key and the customer ID
Answer

The raw request body, the Stripe-Signature header and the endpoint secret — The signature is computed over the exact raw payload.