How to Connect Stripe Webhooks to Your Application

Stripe webhooks allow your application to react to payment events in real time — things like successful charges, subscription renewals, or failed payments. Instead of polling Stripe's API repeatedly, your server receives an HTTP POST notification the moment something happens. This guide walks you through setting up a secure, production-ready Stripe webhook integration.

How Stripe Webhooks Work

When an event occurs in your Stripe account (e.g., a payment succeeds), Stripe sends an HTTP POST request to a URL you've registered. The request body contains a JSON payload describing the event. Your job is to:

  1. Expose a public HTTPS endpoint on your server
  2. Verify the request came from Stripe (not a spoofed attacker)
  3. Parse the event and take appropriate action
  4. Respond with a 200 OK to acknowledge receipt

Step 1: Register Your Webhook Endpoint in Stripe Dashboard

Go to Stripe Dashboard → Developers → Webhooks and click Add endpoint. Enter your server URL (e.g., https://yourdomain.com/webhooks/stripe) and select the events you want to listen for. Common events include:

  • payment_intent.succeeded
  • payment_intent.payment_failed
  • customer.subscription.created
  • invoice.payment_failed

Once registered, Stripe will show you a Webhook Signing Secret — copy it and store it as an environment variable. Never hardcode it in your source.

Step 2: Create the Webhook Endpoint

Here's a minimal example in Node.js using Express:


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

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

    switch (event.type) {
      case 'payment_intent.succeeded':
        handlePaymentSuccess(event.data.object);
        break;
      // handle other events
    }

    res.json({ received: true });
  }
);

Important: Use express.raw() (not express.json()) for the webhook route. Stripe's signature verification requires the raw, unparsed request body.

Step 3: Verify the Webhook Signature

The stripe.webhooks.constructEvent() call computes an HMAC-SHA256 hash of the request body using your signing secret and compares it to the stripe-signature header. If they don't match, it throws an error — reject the request immediately. This protects you against replay attacks and spoofed payloads.

Step 4: Handle Events Idempotently

Stripe may deliver the same event more than once. Your handler must be idempotent — processing the same event twice should not cause duplicate actions. A common pattern is to store the event.id in your database and skip processing if you've already seen it.

Testing Locally with the Stripe CLI

You don't need a public server to test webhooks during development. Install the Stripe CLI and run:

stripe listen --forward-to localhost:3000/webhooks/stripe

This tunnels webhook events to your local server and also lets you trigger test events with stripe trigger payment_intent.succeeded.

Production Checklist

  • ✅ Always use HTTPS for your webhook endpoint
  • ✅ Verify the Stripe signature on every request
  • ✅ Implement idempotency using event.id
  • ✅ Return 200 OK quickly; process events asynchronously if needed
  • ✅ Log all events for debugging and auditing