How I Built a Stripe Webhook in Node.js (Full Technical Guide)
Webhooks are essential for handling asynchronous events in payment processing systems. Here's my deep dive into building a production-grade Stripe webhook handler in Node.js with proper security, error handling, and architecture.
Understanding the 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:
- Endpoint verification using Stripe signatures
- Event processing pipeline
- Idempotency handling
- Error recovery mechanisms
Initial Setup
First, install required dependencies:
npm install stripe express body-parser crypto
Configure your Stripe instance:
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();
app.use(bodyParser.raw({type: 'application/json'}));
Webhook Endpoint Implementation
Here's the core webhook handler with signature verification:
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
app.post('/webhook', async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
WEBHOOK_SECRET
);
} catch (err) {
console.error(`Webhook signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Process the event
try {
await handleStripeEvent(event);
res.json({received: true});
} catch (err) {
console.error(`Event processing failed: ${err.message}`);
res.status(500).send(`Processing Error: ${err.message}`);
}
});
Event Processing Pipeline
Implement a robust event handler with proper error management:
const eventHandlers = {
'payment_intent.succeeded': async (paymentIntent) => {
// Business logic for successful payment
console.log(`Payment succeeded: ${paymentIntent.id}`);
await fulfillOrder(paymentIntent);
},
'payment_intent.payment_failed': async (paymentIntent) => {
console.log(`Payment failed: ${paymentIntent.id}`);
await handleFailedPayment(paymentIntent);
},
// Add more event handlers as needed
};
async function handleStripeEvent(event) {
const handler = eventHandlers[event.type];
if (!handler) {
console.log(`Unhandled event type: ${event.type}`);
return;
}
try {
await handler(event.data.object);
} catch (err) {
console.error(`Handler for ${event.type} failed:`, err);
throw err; // Rethrow for proper HTTP response
}
}
Idempotency Implementation
Critical for preventing duplicate processing:
const processedEvents = new Set();
async function handleStripeEvent(event) {
// Check for duplicate events
if (processedEvents.has(event.id)) {
console.log(`Duplicate event detected: ${event.id}`);
return;
}
processedEvents.add(event.id);
// Clean up old events (simple in-memory implementation)
// In production, use Redis with TTL
if (processedEvents.size > 1000) {
const oldest = Array.from(processedEvents).slice(-1000);
processedEvents.clear();
oldest.forEach(id => processedEvents.add(id));
}
// Rest of event handling...
}
Production-Grade Enhancements
1. Database Integration
// Using MongoDB as example
const { MongoClient } = require('mongodb');
async function isEventProcessed(eventId) {
const client = await MongoClient.connect(process.env.MONGODB_URI);
const db = client.db('payments');
const count = await db.collection('processed_events')
.countDocuments({ eventId });
await client.close();
return count > 0;
}
async function markEventProcessed(eventId) {
const client = await MongoClient.connect(process.env.MONGODB_URI);
const db = client.db('payments');
await db.collection('processed_events')
.insertOne({ eventId, processedAt: new Date() });
await client.close();
}
2. Retry Logic with Exponential Backoff
async function withRetry(fn, maxRetries = 3, baseDelay = 1000) {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await fn();
} catch (err) {
attempt++;
if (attempt >= maxRetries) throw err;
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Retry ${attempt} in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
3. Queue Integration for Heavy Processing
const { Worker } = require('bull');
// Using Bull queue system
const paymentQueue = new Queue('stripe_webhooks', {
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
}
});
// In your webhook handler:
paymentQueue.add(event.type, {
eventId: event.id,
data: event.data.object
});
// Worker process
const worker = new Worker('stripe_webhooks', async job => {
const { eventId, data } = job.data;
await handleStripeEvent({ id: eventId, data });
});
Testing Your Webhook
Use the Stripe CLI for local testing:
stripe listen --forward-to localhost:3000/webhook
Then trigger test events:
stripe trigger payment_intent.succeeded
Security Best Practices
- Always verify webhook signatures
- Use HTTPS for your webhook endpoint
- Implement rate limiting
- Keep your Stripe API keys secure
- Validate all incoming data
Monitoring and Alerting
// Simple logging wrapper
async function withMonitoring(fn, eventType) {
const start = Date.now();
try {
const result = await fn();
console.log({
event: eventType,
status: 'success',
duration: Date.now() - start
});
return result;
} catch (err) {
console.error({
event: eventType,
status: 'failed',
duration: Date.now() - start,
error: err.message
});
// Integrate with your alerting system here
sendAlert(`Webhook processing failed for ${eventType}`);
throw err;
}
}
Final Production Checklist
- [ ] Signature verification implemented
- [ ] Idempotency handling in place
- [ ] Error recovery mechanisms
- [ ] Proper logging and monitoring
- [ ] Load testing completed
- [ ] Alerting configured
- [ ] Documentation for ops team
This implementation provides a solid foundation for handling Stripe webhooks in Node.js. The architecture is designed to be scalable, secure, and maintainable for production workloads. Remember to adapt the code to your specific business requirements and infrastructure.
🚀 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)