6 min read

How to Set Up Webhook Events in Stripe

If you're building on Stripe, you need to react to events as they happen—a payment succeeds, a card fails, a refund processes. Webhooks let your backend listen for these events instead of constantly checking Stripe's API. Here's the fastest way to get them working.

Create and Register Your Webhook Endpoint

First, tell Stripe where to send events by registering an endpoint URL in your dashboard.

Add an Endpoint in the Dashboard

Go to DevelopersWebhooks in your Stripe dashboard. Click Add an endpoint and paste your public URL (e.g., https://your-domain.com/webhooks/stripe). Select the events you want to listen for—start with payment_intent.succeeded, charge.refunded, and customer.subscription.updated. Stripe gives you a signing secret—save this immediately.

javascript
// Your endpoint receives POST requests from Stripe
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'];
  const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
  
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle the event
  console.log(`Received event: ${event.type}`);
  res.json({received: true});
});
Express endpoint that validates Stripe webhook signatures

Handle Specific Event Types

In your webhook handler, use event.type to filter events and event.data.object to access the resource (payment intent, charge, subscription). Process only what you need—don't handle every event if you don't have use for it.

javascript
// Inside your webhook handler
switch (event.type) {
  case 'payment_intent.succeeded':
    const paymentIntent = event.data.object;
    console.log(`Payment succeeded: ${paymentIntent.id}`);
    // Update your database, send confirmation email, etc.
    break;
    
  case 'charge.refunded':
    const charge = event.data.object;
    console.log(`Refund processed: ${charge.id}`);
    // Update invoice status, notify customer
    break;
    
  case 'customer.subscription.updated':
    const subscription = event.data.object;
    console.log(`Subscription updated: ${subscription.id}`);
    // Update billing status
    break;
}

res.json({received: true});
Switch statement for handling different event types
Watch out: Return a 2xx status immediately. Stripe times out after 30 seconds, then retries the webhook. Do the actual work asynchronously (queue a job) so your endpoint responds fast.

Test Webhooks Locally Before Going Live

Don't deploy untested. Use the Stripe CLI to simulate events on your local machine.

Install and Authenticate the Stripe CLI

Download the Stripe CLI from stripe.com/docs/stripe-cli. Log in with stripe login—it generates a restricted key and stores it locally. This lets you forward webhook events to localhost.

javascript
// Terminal commands (not JavaScript, but showing the workflow):
// brew install stripe/stripe-cli/stripe  (or download for Windows/Linux)
// stripe login
// Enter your email and approve the login on the dashboard

// The CLI stores auth locally, no API key needed in your code
// Just run the listen command in a terminal window
Installation and authentication via terminal

Forward Webhooks to Your Local Server

In a terminal, run stripe listen --forward-to localhost:3000/webhooks/stripe. The CLI prints a signing secret (starts with whsec_test_)—add this to your .env.local as STRIPE_WEBHOOK_SECRET. Now trigger a test event in another terminal with stripe trigger payment_intent.succeeded.

javascript
// In your .env.local file:
STRIPE_WEBHOOK_SECRET=whsec_test_YourSecretHere
STRIPE_SECRET_KEY=sk_test_...

// Start your local server (e.g., npm run dev on port 3000)
// Then in another terminal:
// stripe listen --forward-to localhost:3000/webhooks/stripe

// In a third terminal, trigger a test event:
// stripe trigger payment_intent.succeeded

// Your webhook handler receives the event and processes it
Using multiple terminal windows to test webhooks
Tip: The stripe listen command runs in the foreground and shows every webhook in real time. Great for debugging. When you're done, press Ctrl+C to stop it.

Verify Signatures and Deploy to Production

Never skip signature verification. Always validate that requests actually come from Stripe.

Validate Webhook Signatures with constructEvent

The stripe.webhooks.constructEvent() method validates the signature automatically. It throws an error if the request is forged. Never process a webhook without validating it first—this is how you prevent malicious requests.

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

app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  
  try {
    // constructEvent validates the X-Stripe-Signature header
    // and the request body. Throws if signature doesn't match.
    const event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
    
    // Only process verified events
    handleEvent(event);
    res.json({received: true});
  } catch (err) {
    console.error('Invalid webhook signature:', err.message);
    res.status(400).send('Webhook signature verification failed');
  }
});
Full signature validation protecting against forged requests

Rotate Your Webhook Secret Periodically

In the Webhooks section of the Stripe dashboard, click your endpoint and rotate the signing secret. Update your .env with the new secret, deploy your changes, then confirm the rotation. The old secret stops working immediately.

javascript
// Rotation workflow:
// 1. Get new secret from Stripe dashboard
// 2. Update .env:
process.env.STRIPE_WEBHOOK_SECRET = 'whsec_live_newSecretHere';

// 3. Deploy your code
// 4. Return to Stripe and confirm rotation
// 5. Old secret no longer works

// Your webhook handler now uses the new secret:
const event = stripe.webhooks.constructEvent(
  req.body,
  sig,
  process.env.STRIPE_WEBHOOK_SECRET  // Now the new secret
);
Safe rotation process that avoids downtime
Watch out: Stripe retries failed webhooks up to 3 times over 24 hours. If your endpoint is offline or returns an error, you'll miss the event. Stripe doesn't re-send old events, so monitor uptime carefully.

Common Pitfalls

  • Skipping signature validation—always use constructEvent() to verify the request came from Stripe, never trust the raw body
  • Doing too much work before returning a response—Stripe times out after 30 seconds, so respond immediately and process asynchronously
  • Not keeping the webhook secret secure—if it leaks, anyone can forge Stripe events; rotate it regularly and never commit it to git
  • Enabling every Stripe event in the dashboard—you'll get flooded with events you don't need; only subscribe to what you actually handle

Wrapping Up

You now have webhooks listening for Stripe events, validating them properly, and processing them in real time. This is how modern payment integrations sync charges, subscriptions, and refunds with your backend automatically. If you want to track this automatically across tools, Product Analyst can help.

Track these metrics automatically

Product Analyst connects to your stack and surfaces the insights that matter.

Try Product Analyst — Free