How I Built a Stripe Webhook in Node.js (Full Guide)
Webhooks are essential for handling asynchronous events in payment processing systems. In this deep dive, I'll show you how to build 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:
- Endpoint Verification: Validating Stripe signatures
- Event Processing: Handling different event types
- Idempotency: Preventing duplicate processing
- Error Handling: Managing failed deliveries
Initial Setup
First, install required packages:
npm install stripe express body-parser crypto
Basic server setup:
const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const crypto = require('crypto');
const app = express();
// Stripe requires raw body for signature verification
app.post('/webhook', bodyParser.raw({type: 'application/json'}), handleStripeWebhook);
app.listen(4242, () => console.log('Webhook listening on port 4242'));
Signature Verification
Security is paramount. Always verify the webhook signature:
function verifyStripeSignature(req) {
const signature = req.headers['stripe-signature'];
const signingSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
return stripe.webhooks.constructEvent(
req.body,
signature,
signingSecret
);
} catch (err) {
console.error('ā ļø Webhook signature verification failed', err);
throw new Error('Invalid signature');
}
}
Event Processing Architecture
Implement a robust event processor with proper error handling:
const eventHandlers = {
'payment_intent.succeeded': handleSuccessfulPayment,
'charge.failed': handleFailedCharge,
'invoice.payment_succeeded': handleSubscriptionPayment,
// Add more event types as needed
};
async function handleStripeWebhook(req, res) {
let event;
try {
event = verifyStripeSignature(req);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
const handler = eventHandlers[event.type];
if (handler) {
try {
await handler(event);
res.json({received: true});
} catch (err) {
console.error(`Error processing ${event.type}`, err);
res.status(500).send('Internal Server Error');
}
} else {
res.status(400).send(`Unhandled event type: ${event.type}`);
}
}
Implementing Idempotency
Stripe may retry failed webhook deliveries. Use idempotency keys:
const processedEvents = new Set();
async function processEventWithIdempotency(event) {
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed`);
return;
}
// Process the event
await handleEventLogic(event);
// Store processed event ID
processedEvents.add(event.id);
// Optional: Implement TTL for memory management
setTimeout(() => processedEvents.delete(event.id), 24 * 60 * 60 * 1000);
}
Database Integration
For production, use a database to track processed events:
const { MongoClient } = require('mongodb');
async function isEventProcessed(eventId) {
const client = new MongoClient(process.env.MONGODB_URI);
try {
await client.connect();
const db = client.db('stripe_webhooks');
const count = await db.collection('processed_events')
.countDocuments({ eventId });
return count > 0;
} finally {
await client.close();
}
}
async function markEventAsProcessed(eventId) {
const client = new MongoClient(process.env.MONGODB_URI);
try {
await client.connect();
const db = client.db('stripe_webhooks');
await db.collection('processed_events')
.insertOne({ eventId, processedAt: new Date() });
} finally {
await client.close();
}
}
Handling Specific Events
Example implementation for payment success:
async function handleSuccessfulPayment(event) {
const paymentIntent = event.data.object;
// Business logic
await fulfillOrder(paymentIntent.metadata.orderId);
await sendConfirmationEmail(paymentIntent.customer);
console.log(`Payment for ${paymentIntent.amount} succeeded`);
}
async function fulfillOrder(orderId) {
// Update your database, trigger shipping, etc.
console.log(`Fulfilling order ${orderId}`);
}
Error Handling and Retries
Implement exponential backoff for failed operations:
async function withRetry(fn, maxAttempts = 3) {
let attempt = 0;
while (attempt < maxAttempts) {
try {
return await fn();
} catch (err) {
attempt++;
if (attempt >= maxAttempts) throw err;
const delay = Math.pow(2, attempt) * 1000;
console.log(`Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Testing Webhooks Locally
Use the Stripe CLI for local testing:
stripe listen --forward-to localhost:4242/webhook
Trigger test events:
stripe trigger payment_intent.succeeded
Production Considerations
- Rate Limiting: Protect your endpoint from abuse
- Queue Processing: Use Redis or RabbitMQ for high volume
- Monitoring: Track successful/failed webhooks
- Logging: Record all incoming events for debugging
Example queue implementation:
const { Queue } = require('bull');
const webhookQueue = new Queue('stripe_webhooks', {
redis: process.env.REDIS_URL
});
webhookQueue.process(async job => {
const { event } = job.data;
const handler = eventHandlers[event.type];
if (handler) await handler(event);
});
// In your webhook handler:
await webhookQueue.add({ event });
Complete Example
Here's the full implementation:
const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const { Queue } = require('bull');
const app = express();
const webhookQueue = new Queue('stripe_webhooks', {
redis: process.env.REDIS_URL
});
const eventHandlers = {
'payment_intent.succeeded': async (event) => {
const paymentIntent = event.data.object;
await fulfillOrder(paymentIntent.metadata.orderId);
},
// Other event handlers...
};
function verifyStripeSignature(req) {
const signature = req.headers['stripe-signature'];
return stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
}
app.post('/webhook',
bodyParser.raw({type: 'application/json'}),
async (req, res) => {
let event;
try {
event = verifyStripeSignature(req);
await webhookQueue.add({ event });
res.json({received: true});
} catch (err) {
console.error('Webhook error:', err);
res.status(400).send(`Webhook Error: ${err.message}`);
}
}
);
app.listen(4242, () => console.log('Webhook listening on port 4242'));
Final Thoughts
Building a robust Stripe webhook handler requires attention to:
- Security (signature verification)
- Reliability (idempotency, retries)
- Scalability (queue processing)
- Maintainability (clear event handling structure)
This implementation gives you a production-ready foundation that you can extend with your specific business logic. Remember to monitor your webhook processing and set up alerts for failed deliveries.
š 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 (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.