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.
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'));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.
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);
});
});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.
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});
});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.
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});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.
// 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');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.
// 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 webhookCommon Pitfalls
- Skipping signature verification—always use
stripe.webhooks.constructEvent()to validate that the webhook came from Stripe. - Not handling duplicate events—store the
event.idin 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.