Tutorials / Backend integrations / Set up Stripe webhooks
📝 Written ● Intermediate Updated 2026-05-13

Set up Stripe webhooks

Webhooks are how Stripe tells your server when a payment happens. Add an endpoint, verify the signature, test locally with the Stripe CLI — about ten minutes end to end.

Add the webhook endpoint

1

Open the Stripe Dashboard. Toggle to Test mode (orange banner) while developing.

2

Go to Developers → Webhooks. Click Add endpoint.

3

Enter your webhook URL:

https://yourdomain.com/api/stripe/webhook
4

Select events. Common ones:

  • checkout.session.completed — Stripe Checkout finished
  • payment_intent.succeeded — custom payment flow succeeded
  • invoice.paid / invoice.payment_failed — subscriptions
  • customer.subscription.deleted — subscription ended
  • charge.refunded — charge refunded

Full list: Stripe event types reference ↗. Start with one or two; add more as you grow.

5

Click Add endpoint. Stripe shows the Signing secret:

whsec_xxxxxxxxxxxxxxxxxxxxxx

Copy it. Store in your backend env:

STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxx

The handler (Node + Express)

6
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();

app.post(
  '/api/stripe/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['stripe-signature'];

    try {
      const event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      );

      if (event.type === 'checkout.session.completed') {
        console.log('Payment successful', event.data.object);
        // Grant access, send email, etc.
      }

      res.sendStatus(200);
    } catch (err) {
      console.error('Webhook error:', err.message);
      res.sendStatus(400);
    }
  }
);

SDK reference: stripe-node ↗.

Use express.raw(), not express.json(), on the webhook route. Signature verification reads the raw bytes. If a JSON middleware parses the body first, verification fails.

Test locally with the Stripe CLI

7

Install (Stripe CLI docs ↗):

brew install stripe/stripe-cli/stripe
stripe login

Forward events to your local server:

stripe listen --forward-to localhost:3000/api/stripe/webhook

The CLI prints a webhook signing secret (separate from the dashboard's). Use it as STRIPE_WEBHOOK_SECRET in your local .env.

8

Trigger a test event:

stripe trigger checkout.session.completed

Your local handler should receive and verify it.

Make it idempotent

9

Stripe retries on failure. The same event ID can arrive multiple times. Dedupe on event.id:

const exists = await db.processedEvents.findOne({ id: event.id });
if (exists) return res.sendStatus(200);  // Already handled

// ... process event ...

await db.processedEvents.insert({ id: event.id });

Go live

10

Test mode and live mode have separate webhook endpoints. When you're ready:

  1. Toggle the dashboard to Live mode.
  2. Repeat Steps 1–5 with your production URL.
  3. Set the live signing secret as STRIPE_WEBHOOK_SECRET in your production env.
Other stacks. Stripe ships constructEvent in all official SDKs — same pattern (raw body, signature header, signing secret). Quick links:

Official references

What's next