How I Built a Stripe Webhook in Node.js (Full Guide)
Webhooks are essential for modern payment processing systems. In this technical deep dive, I'll show you how to implement a production-grade Stripe webhook handler in Node.js with proper security, error handling, and idempotency.
Webhook Architecture Fundamentals
Stripe webhooks use HTTP POST requests to notify your server about events. The critical components we need to implement:
- Endpoint verification - Validate Stripe signatures
- Idempotency - Handle duplicate events
- Error resilience - Queue failed events for retry
- Event processing - Business logic execution
Initial Setup
First, install required dependencies:
npm install stripe express body-parser
Create a basic Express server:
const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
// Stripe requires raw body for signature verification
app.post('/webhook',
bodyParser.raw({type: 'application/json'}),
handleStripeWebhook
);
app.listen(4242, () => console.log('Listening on port 4242'));
Signature Verification
Security is critical - we must verify requests actually come from Stripe:
async function handleStripeWebhook(req, res) {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} 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 with Idempotency
We need to handle duplicate events safely:
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;
}
try {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'charge.failed':
await handleChargeFailure(event.data.object);
break;
// Add other event types as needed
default:
console.log(`Unhandled event type ${event.type}`);
}
// Mark event as processed
processedEvents.add(event.id);
} catch (err) {
console.error(`Error processing event ${event.id}:`, err);
throw err; // Will trigger Stripe's automatic retry
}
}
Handling Payment Events
Here's how to implement business logic for common events:
async function handlePaymentSuccess(paymentIntent) {
// Extract relevant data
const { id, amount, customer, metadata } = paymentIntent;
console.log(`Payment ${id} succeeded for ${amount}`);
// Update your database
await db.payments.updateOne(
{ paymentId: id },
{ $set: { status: 'completed' } }
);
// Fulfill order
if (metadata.orderId) {
await fulfillOrder(metadata.orderId);
}
}
async function handleChargeFailure(charge) {
console.log(`Charge failed: ${charge.id}`);
// Notify customer
if (charge.customer) {
await sendFailedPaymentEmail(charge.customer);
}
}
Production Considerations
1. Event Queueing
For production, use a proper queue system instead of in-memory Set:
const { Worker } = require('bullmq');
const eventQueue = new Worker('stripe-events', async job => {
const event = job.data;
await processEvent(event);
}, {
connection: redisClient,
limiter: { max: 10, duration: 1000 } // Rate limiting
});
2. Database Integration
Store events for auditing:
async function storeEvent(event) {
await db.events.insertOne({
id: event.id,
type: event.type,
data: event.data,
created: new Date(event.created * 1000),
processedAt: new Date()
});
}
3. Error Handling and Retries
Implement exponential backoff for failed events:
async function processWithRetry(event, attempt = 1) {
try {
await processEvent(event);
} catch (err) {
if (attempt >= 3) {
await db.failedEvents.insertOne({ event, error: err.message });
return;
}
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return processWithRetry(event, attempt + 1);
}
}
Testing Webhooks Locally
Use the Stripe CLI to test:
stripe listen --forward-to localhost:4242/webhook
Trigger test events:
stripe trigger payment_intent.succeeded
Complete Implementation
Here's the full production-ready version:
const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const { Worker } = require('bullmq');
const redis = require('redis');
const app = express();
const redisClient = redis.createClient(process.env.REDIS_URL);
// Event queue for processing
const eventQueue = new Worker('stripe-events', processEventJob, {
connection: redisClient
});
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
);
// Add to queue for processing
await eventQueue.add(event.id, event);
res.json({received: true});
} catch (err) {
console.error(`Webhook error: ${err.message}`);
res.status(400).send(`Webhook Error: ${err.message}`);
}
}
);
async function processEventJob(job) {
const event = job.data;
try {
await storeEvent(event);
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
// Other event handlers...
}
} catch (err) {
console.error(`Failed to process event ${event.id}:`, err);
throw err; // Will trigger BullMQ's retry mechanism
}
}
app.listen(4242, () => console.log('Listening on port 4242'));
Key Takeaways
- Always verify webhook signatures
- Implement idempotency to prevent duplicate processing
- Use queue systems for reliable event processing
- Store events for auditing and debugging
- Implement proper error handling and retries
This implementation handles all critical aspects of production-grade Stripe webhook processing while maintaining security and reliability. The queuing system ensures no events are lost during failures, and the idempotency checks prevent duplicate processing.
Remember to monitor your webhook processing and set up alerts for failed events that exceed your retry thresholds.
🚀 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)