Webhook events are how Stripe notifies your application when something happens in your account—a payment succeeds, a subscription renews, a customer is created. Without webhooks, you'd poll Stripe's API every few seconds to check for updates, which is inefficient and will get you rate-limited. Webhooks are the backbone of any production Stripe integration.
Understanding Webhook Events
A webhook event is a JSON payload that Stripe sends to your application in real time when an action occurs. Each event has a type, ID, and a data object containing the resource that triggered it.
See what a webhook event looks like
When Stripe fires an event, it sends an HTTP POST to your endpoint with a JSON payload. Each event has a type (like payment_intent.succeeded), a created timestamp, and a data object containing the actual resource (charge, customer, etc.).
// This is what Stripe sends to your webhook endpoint
const event = {
id: 'evt_1234567890',
object: 'event',
api_version: '2023-10-16',
created: 1234567890,
type: 'payment_intent.succeeded',
data: {
object: {
id: 'pi_1234567890',
amount: 5000,
currency: 'usd',
customer: 'cus_1234567890',
status: 'succeeded'
}
}
};Retrieve a specific event from the Events API
You can query past events using the Stripe API. This is useful for debugging or replaying events that were missed. Use the stripe.events.retrieve() method with an event ID.
const stripe = require('stripe')('sk_test_...');
const event = await stripe.events.retrieve('evt_1234567890');
console.log(`Event type: ${event.type}`);
console.log(`Created: ${event.created}`);
console.log(`Data:`, event.data.object);id (starting with evt_). Store these IDs in your database to deduplicate and prevent processing the same event twice.Creating and Registering Webhook Endpoints
A webhook endpoint is a URL on your server where Stripe sends events. You can create and manage endpoints programmatically via the API or manually in the Stripe Dashboard under Developers > Webhooks.
Create a webhook endpoint via the Stripe API
Use stripe.webhookEndpoints.create() to register a new endpoint and specify which events you want to receive. You can set up specific event types like charge.succeeded or customer.created.
const stripe = require('stripe')('sk_test_...');
const endpoint = await stripe.webhookEndpoints.create({
url: 'https://example.com/webhook/stripe',
enabled_events: [
'charge.succeeded',
'charge.failed',
'customer.created',
'invoice.payment_succeeded'
]
});
console.log(`Webhook created: ${endpoint.id}`);
console.log(`Secret: ${endpoint.secret}`); // Store this securelySet up your endpoint handler in Node.js
Create an Express route that receives POST requests from Stripe. Verify the webhook signature to confirm the request came from Stripe, then parse and process the event.
const express = require('express');
const stripe = require('stripe')('sk_test_...');
const app = express();
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
// Use raw body for signature verification
app.post('/webhook/stripe', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
// Handle the event
if (event.type === 'charge.succeeded') {
const charge = event.data.object;
console.log(`Charge ${charge.id} succeeded for $${charge.amount / 100}`);
}
res.json({received: true});
} catch (err) {
console.error(`Webhook error: ${err.message}`);
res.status(400).send(`Webhook Error: ${err.message}`);
}
});
app.listen(3000, () => console.log('Webhook server running'));List and manage your endpoints
Query all webhook endpoints in your Stripe account using stripe.webhookEndpoints.list(). This is helpful for auditing which endpoints are active and what events they're listening for.
const stripe = require('stripe')('sk_test_...');
const endpoints = await stripe.webhookEndpoints.list();
for (const endpoint of endpoints.data) {
console.log(`URL: ${endpoint.url}`);
console.log(`Status: ${endpoint.status}`);
console.log(`Events: ${endpoint.enabled_events.join(', ')}`);
}stripe.webhooks.constructEvent(). Never trust the event without verification—anyone can POST to your endpoint.Processing Specific Webhook Events
Different events require different handling. A charge.succeeded event means you should mark an order as paid. A customer.subscription.deleted event means you should downgrade or revoke access.
Handle payment success and failure events
Listen for charge.succeeded and charge.failed to update your order status. The charge object contains the amount, currency, and customer details. Update your database to reflect the outcome.
app.post('/webhook/stripe', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
switch(event.type) {
case 'charge.succeeded':
const charge = event.data.object;
await db.orders.update({stripe_charge_id: charge.id}, {status: 'paid'});
break;
case 'charge.failed':
const failedCharge = event.data.object;
console.log(`Payment failed: ${failedCharge.failure_message}`);
await db.orders.update({stripe_charge_id: failedCharge.id}, {status: 'failed'});
break;
}
res.json({received: true});
});Handle subscription events
Listen for customer.subscription.updated and customer.subscription.deleted to sync subscription status. This ensures your user's access level stays in sync with Stripe whenever they upgrade, downgrade, or cancel.
case 'customer.subscription.updated':
const subscription = event.data.object;
await db.subscriptions.update(
{stripe_subscription_id: subscription.id},
{
status: subscription.status,
current_period_end: new Date(subscription.current_period_end * 1000)
}
);
break;
case 'customer.subscription.deleted':
const deletedSub = event.data.object;
await db.users.update(
{stripe_customer_id: deletedSub.customer},
{subscription_status: 'cancelled'}
);
break;Common Pitfalls
- Not verifying webhook signatures — stripe.webhooks.constructEvent() will throw if the signature is invalid, preventing spoofed requests from reaching your logic
- Using parsed JSON instead of raw body for signature verification — Stripe signs the raw request body, so you must pass the raw bytes to constructEvent(), not a parsed JSON string
- Processing the same event multiple times — Stripe retries events if your endpoint doesn't respond with a 2xx status. Always deduplicate on event.id before updating your database
- Not returning a 2xx status code quickly — Stripe expects a response within 5 seconds. Do async work in a background queue, not in the webhook handler itself
Wrapping Up
Webhook events are Stripe's way of keeping your application in sync with payments in real time. By setting up endpoints, verifying signatures, and handling specific event types, you've built a reliable payment system that responds instantly to customer actions. If you want to track this automatically across tools, Product Analyst can help.