6 min read

How to Track Webhook Events in Stripe

Stripe sends webhook events whenever something happens in your account—a payment succeeds, a customer is created, or a subscription is renewed. If you're not tracking these events, you're missing critical data about what's actually happening in your business. Here's how to listen for and log them.

Setting Up Your Webhook Endpoint

First, you need an HTTPS endpoint in your application that can receive webhook payloads from Stripe.

Create an endpoint to receive webhook POST requests

Your endpoint should accept POST requests and be accessible over HTTPS. This is where Stripe will send all webhook events. For development, you can use Stripe CLI to forward events to localhost.

javascript
const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// Raw body needed to verify Stripe signature
app.post('/webhook', bodyParser.raw({type: 'application/json'}), (req, res) => {
  const event = req.body;
  console.log('Webhook received:', event.type);
  res.json({received: true});
});

app.listen(3000, () => console.log('Webhook server running'));
Basic webhook endpoint setup

Add webhook endpoint in Stripe Dashboard

Go to Developers > Webhooks in the Stripe Dashboard. Click Add endpoint and enter your endpoint URL (e.g., https://yourapp.com/webhook). Select the events you want to listen for. Common ones: payment_intent.succeeded, payment_intent.payment_failed, customer.subscription.created, invoice.payment_succeeded.

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

// List existing webhooks to verify setup
stripe.webhookEndpoints.list()
  .then(endpoints => {
    endpoints.data.forEach(endpoint => {
      console.log('Endpoint:', endpoint.url, 'Events:', endpoint.enabled_events);
    });
  });
List webhook endpoints to verify setup
Watch out: Make sure your endpoint returns a 2xx status code within 30 seconds. Stripe will retry failed webhooks for 3 days.

Verifying Webhook Signatures and Logging Events

Stripe signs every webhook with a secret key. Always verify the signature to confirm the request actually came from Stripe.

Verify the webhook signature using your signing secret

Get your Signing secret from the Developers > Webhooks page in Stripe. Use stripe.webhooks.constructEvent() to verify the signature. This confirms the webhook is legitimate.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const bodyParser = require('body-parser');

app.post('/webhook', bodyParser.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
  
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  console.log('✓ Verified webhook event:', event.type, 'ID:', event.id);
  res.json({received: true});
});
Verify webhook signature and safely parse the event

Parse the event and log the relevant data

Once verified, extract the event type and data. Store event details in your database or logging system. Log the event.type, event.id, and the actual object inside event.data.object.

javascript
switch (event.type) {
  case 'payment_intent.succeeded':
    const paymentIntent = event.data.object;
    console.log(`Payment received: ${paymentIntent.amount / 100} ${paymentIntent.currency.toUpperCase()}`);
    console.log('Customer ID:', paymentIntent.customer);
    await db.webhookEvents.create({
      stripeEventId: event.id,
      type: event.type,
      customerId: paymentIntent.customer,
      amount: paymentIntent.amount,
      timestamp: new Date(event.created * 1000)
    });
    break;
  
  case 'customer.subscription.created':
    const subscription = event.data.object;
    console.log(`Subscription created: ${subscription.id}`);
    break;
  
  default:
    console.log(`Unhandled event type: ${event.type}`);
}

res.json({received: true});
Handle different webhook types and log the data
Tip: Always log the event.id to prevent duplicate processing. If Stripe retries the same webhook, it uses the same event.id.

Testing Webhooks Locally and in Production

Before shipping, test that your webhook handler works correctly and handles retries gracefully.

Use Stripe CLI to test webhooks locally

Install the Stripe CLI, then use stripe listen to forward live Stripe events to your local endpoint. This works without exposing localhost to the internet.

javascript
// In your terminal:
// 1. Install Stripe CLI: brew install stripe/stripe-cli/stripe
// 2. Authenticate: stripe login
// 3. Forward webhooks to your local server:
// stripe listen --forward-to localhost:3000/webhook
// This outputs a signing secret, add to .env as STRIPE_WEBHOOK_SECRET

// Trigger a test event in another terminal:
// stripe trigger payment_intent.succeeded

// Your webhook endpoint will immediately receive and log the event
console.log('Webhook received during local testing');
Stripe CLI command to test webhooks locally

Verify delivery logs in the Stripe Dashboard

In the Stripe Dashboard, go to Developers > Webhooks and click your endpoint. The Logs tab shows all webhook deliveries, timestamps, and response status codes. Verify that your endpoint returns 200 for successful events.

javascript
// Test idempotency by intentionally failing and observing retries
app.post('/webhook', bodyParser.raw({type: 'application/json'}), (req, res) => {
  const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
  
  // Simulate a failure to test retry behavior
  if (process.env.TEST_WEBHOOK_FAILURE === 'true') {
    return res.status(500).send('Simulated error - testing retry');
  }
  
  // Process event normally
  console.log('Event processed:', event.id);
  res.json({received: true});
});

// Watch the Logs tab to see Stripe retry the failed webhook
Simulate webhook failure to test Stripe's retry mechanism
Watch out: Stripe retries webhooks with exponential backoff over 3 days. If your endpoint is down, you may miss events. Always sync missed events by querying the Stripe API directly or backfilling from event logs.

Common Pitfalls

  • Skipping signature verification—always use stripe.webhooks.constructEvent() to validate that the webhook came from Stripe.
  • Not handling duplicate events—store the event.id in your database and check it before processing, since Stripe may retry the same event.
  • Ignoring webhook logs in the Dashboard—the Logs tab shows delivery status and response codes; use it to debug failed webhooks.
  • Processing events synchronously in your request handler—if your webhook handler takes too long, Stripe times out after 30 seconds and retries. Use queues (Bull, RabbitMQ) for heavy processing.

Wrapping Up

You now have a secure webhook handler that verifies Stripe's signature, logs events, and handles retries. Start by listening to a few critical events—like payment_intent.succeeded and customer.subscription.created—then expand from there. 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