How I Built a Stripe Webhook in Node.js (Full Technical Guide)
Webhooks are essential for handling asynchronous events in payment processing systems. In this technical deep dive, I'll walk through building a production-grade Stripe webhook handler in Node.js with proper security, error handling, and idempotency.
Understanding Stripe Webhook Architecture
Stripe webhooks use HTTP POST requests to notify your server about events like:
- Successful payments (
payment_intent.succeeded) - Failed charges (
charge.failed) - Subscription changes (
customer.subscription.updated)
The critical components we'll implement:
- Webhook endpoint verification
- Event processing pipeline
- Idempotency handling
- Error recovery mechanisms
Initial Setup
First, install required dependencies:
npm install stripe express body-parser crypto
Configure your Express server with the webhook endpoint:
const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
// Use raw body for webhook verification
app.post('/webhook', bodyParser.raw({type: 'application/json'}), handleStripeWebhook);
app.listen(4242, () => console.log('Listening on port 4242'));
Webhook Verification Security
The most critical security measure is verifying webhook signatures:
async function handleStripeWebhook(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}`);
}
// Process the verified event
await processEvent(event);
res.json({received: true});
}
Event Processing Pipeline
Implement a robust event processor with error handling:
const eventHandlers = {
'payment_intent.succeeded': handleSuccessfulPayment,
'payment_intent.payment_failed': handleFailedPayment,
'invoice.payment_succeeded': handleSubscriptionPayment,
// Add more event handlers as needed
};
async function processEvent(event) {
const handler = eventHandlers[event.type];
if (!handler) {
console.warn(`Unhandled event type: ${event.type}`);
return;
}
try {
await handler(event);
} catch (err) {
console.error(`Error processing ${event.type}`, err);
// Implement your retry logic here
throw err;
}
}
Idempotency Implementation
To prevent duplicate processing:
const processedEvents = new Set();
async function processEvent(event) {
// Check for duplicate events
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed`);
return;
}
// Process the event
await eventHandlers[event.type](event);
// Store the processed event ID
processedEvents.add(event.id);
// Optional: Implement TTL for memory cleanup
setTimeout(() => processedEvents.delete(event.id), 24 * 60 * 60 * 1000);
}
For production, replace the in-memory Set with Redis:
const redis = require('redis');
const client = redis.createClient();
async function isEventProcessed(eventId) {
return new Promise((resolve) => {
client.exists(`stripe_event:${eventId}`, (err, reply) => {
resolve(reply === 1);
});
});
}
async function markEventProcessed(eventId) {
client.setex(`stripe_event:${eventId}`, 86400, 'processed');
}
Handling Payment Events
Example payment handler implementation:
async function handleSuccessfulPayment(event) {
const paymentIntent = event.data.object;
// Important business logic
await fulfillOrder(paymentIntent.metadata.orderId);
// Update your database
await updatePaymentStatus(
paymentIntent.id,
'succeeded',
paymentIntent.amount
);
console.log(`Payment for ${paymentIntent.amount} succeeded`);
}
async function fulfillOrder(orderId) {
// Implement your order fulfillment logic
// This might involve:
// - Updating inventory
// - Sending confirmation emails
// - Triggering shipping processes
}
Error Handling and Retries
Implement exponential backoff for failed events:
async function processEventWithRetry(event, attempt = 1) {
try {
await processEvent(event);
} catch (err) {
if (attempt >= 3) {
console.error(`Final attempt failed for ${event.id}`);
await storeFailedEvent(event);
return;
}
const delay = Math.pow(2, attempt) * 1000;
console.log(`Retrying ${event.id} in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
await processEventWithRetry(event, attempt + 1);
}
}
Testing Webhooks Locally
Use the Stripe CLI for local testing:
stripe listen --forward-to localhost:4242/webhook
Then trigger test events:
stripe trigger payment_intent.succeeded
Production Considerations
- Scale: Use queue systems (Bull, RabbitMQ) for high-volume webhooks
- Monitoring: Implement logging and alerts for failed webhooks
- Redundancy: Set up multiple webhook endpoints for critical events
- Versioning: Handle API version differences with Stripe's version header
Complete Webhook Handler Example
Here's the full implementation:
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const bodyParser = require('body-parser');
const redis = require('redis');
const app = express();
const redisClient = redis.createClient(process.env.REDIS_URL);
const eventHandlers = {
'payment_intent.succeeded': handleSuccessfulPayment,
'payment_intent.payment_failed': handleFailedPayment,
'invoice.payment_succeeded': handleSubscriptionPayment
};
app.post('/webhook',
bodyParser.raw({type: 'application/json'}),
async (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
await processEventWithRetry(event);
res.json({received: true});
} catch (err) {
console.error('Webhook error:', err);
res.status(400).send(`Webhook Error: ${err.message}`);
}
}
);
async function processEventWithRetry(event, attempt = 1) {
if (await isEventProcessed(event.id)) return;
try {
const handler = eventHandlers[event.type];
if (handler) await handler(event);
await markEventProcessed(event.id);
} catch (err) {
if (attempt >= 3) {
await storeFailedEvent(event);
return;
}
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 1000));
await processEventWithRetry(event, attempt + 1);
}
}
// Start server
app.listen(4242, () => console.log('Webhook handler running'));
Performance Optimization
For high-traffic systems:
- Implement event batching
- Use worker pools for CPU-intensive operations
- Consider database connection pooling
const { Worker, isMainThread, workerData } = require('worker_threads');
if (!isMainThread) {
// Worker thread processing logic
processEvent(workerData.event);
}
// In your webhook handler:
if (requiresHeavyProcessing(event)) {
new Worker('./eventWorker.js', { workerData: { event } });
} else {
processEvent(event);
}
Conclusion
This implementation provides:
- Secure webhook verification
- Reliable event processing
- Idempotent operations
- Comprehensive error handling
- Scalability considerations
Remember to always:
- Verify webhook signatures
- Handle events idempotently
- Implement proper error handling
- Monitor your webhook processing
The complete code is production-ready and can be extended with additional event types and business logic as needed.
🚀 Stop Writing Boilerplate Prompts
If you want to skip the setup and code 10x faster with complete AI architecture patterns, grab my Senior React Developer AI Cookbook ($19). It includes Server Action prompt libraries, UI component generation loops, and hydration debugging strategies.
Browse all 10+ developer products at the Apollo AI Store | Or snipe Solana tokens free via @ApolloSniper_Bot.
Top comments (0)