How I Built a Stripe Webhook in Node.js (Full Technical Guide)
Webhooks are essential for modern payment processing systems, and Stripe's implementation is one of the most robust in the industry. In this deep dive, I'll walk through building a production-grade Stripe webhook handler in Node.js that handles events securely and efficiently.
Understanding Stripe Webhook Architecture
Stripe webhooks operate on a push model - when events occur in Stripe's systems (like successful payments or failed charges), they send HTTP POST requests to your configured endpoint. The critical components:
- Event Object: JSON payload containing event metadata and relevant object (charge, customer, etc.)
- Signature Verification: HMAC signature to verify request authenticity
- Idempotency: Handling duplicate events safely
Initial Setup
First, install the required dependencies:
npm install stripe express body-parser crypto-js
Here's our basic server structure:
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();
// Middleware to get raw body for signature verification
app.use(bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
app.listen(PORT, () => console.log(`Webhook listener on port ${PORT}`));
Signature Verification
The most critical security aspect is verifying the webhook signature:
function verifyStripeSignature(req) {
const signature = req.headers['stripe-signature'];
if (!signature) throw new Error('No signature found');
const signedPayload = `${signature.t},${req.rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload, 'utf8')
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature.v1, 'hex'),
Buffer.from(expectedSignature, 'hex')
)) {
throw new Error('Invalid signature');
}
}
Event Processing Architecture
For production systems, we need robust event handling:
// Event processor map
const eventHandlers = {
'payment_intent.succeeded': handleSuccessfulPayment,
'charge.failed': handleFailedCharge,
'customer.subscription.deleted': handleSubscriptionCanceled,
// Add more event types as needed
};
async function processStripeEvent(event) {
const handler = eventHandlers[event.type];
if (!handler) {
console.warn(`Unhandled event type: ${event.type}`);
return { processed: false };
}
try {
await handler(event);
return { processed: true };
} catch (err) {
console.error(`Error processing ${event.type}:`, err);
throw err;
}
}
Implementing Idempotency
Stripe may send duplicate events - our handlers must be idempotent:
const processedEvents = new Set();
async function handleWebhook(req, res) {
try {
verifyStripeSignature(req);
const event = req.body;
// Check for duplicate events
if (processedEvents.has(event.id)) {
return res.status(200).json({ received: true });
}
// Process the event
const result = await processStripeEvent(event);
// Store processed event ID
if (result.processed) {
processedEvents.add(event.id);
// In production, use Redis or database for persistence
}
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook error:', err);
res.status(400).json({ error: err.message });
}
}
app.post('/webhook', handleWebhook);
Example Event Handlers
Let's implement concrete handlers for common scenarios:
1. Successful Payment Handler
async function handleSuccessfulPayment(event) {
const paymentIntent = event.data.object;
// Business logic implementation
await fulfillOrder(paymentIntent.metadata.orderId);
// Log for analytics
trackPaymentEvent(paymentIntent);
// Update database
await updateOrderStatus(paymentIntent.metadata.orderId, 'paid');
}
2. Failed Charge Handler
async function handleFailedCharge(event) {
const charge = event.data.object;
// Notify customer
await sendPaymentFailedEmail(charge.customer);
// Update inventory or retry logic
await releaseInventoryHold(charge.metadata.orderId);
// Log for fraud analysis
logFailedPaymentAttempt(charge);
}
Testing Webhooks Locally
Use the Stripe CLI for local testing:
stripe listen --forward-to localhost:3000/webhook
Trigger test events:
stripe trigger payment_intent.succeeded
Production Considerations
- Error Handling: Implement retry logic with exponential backoff
- Queue Processing: Use Redis or SQS for event queuing
- Logging: Structured logging for all events
- Monitoring: Alert on failed webhook deliveries
- Scaling: Horizontal scaling with proper event deduplication
Complete Webhook Handler
Here's the full implementation:
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();
app.use(bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
const processedEvents = new Set();
// Verification middleware
const verifyStripeWebhook = (req, res, next) => {
try {
const signature = req.headers['stripe-signature'];
if (!signature) throw new Error('No signature found');
const signedPayload = `${signature.t},${req.rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload, 'utf8')
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature.v1, 'hex'),
Buffer.from(expectedSignature, 'hex')
)) {
throw new Error('Invalid signature');
}
next();
} catch (err) {
res.status(403).json({ error: err.message });
}
};
// Event handlers
const eventHandlers = {
'payment_intent.succeeded': async (event) => {
const paymentIntent = event.data.object;
console.log(`Payment succeeded for ${paymentIntent.amount}`);
// Implement your business logic
},
'charge.failed': async (event) => {
const charge = event.data.object;
console.warn(`Charge failed for ${charge.amount}`);
// Implement failure handling
}
};
// Webhook endpoint
app.post('/webhook', verifyStripeWebhook, async (req, res) => {
const event = req.body;
if (processedEvents.has(event.id)) {
return res.status(200).json({ received: true });
}
try {
const handler = eventHandlers[event.type];
if (handler) {
await handler(event);
processedEvents.add(event.id);
}
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook processing error:', err);
res.status(400).json({ error: 'Processing failed' });
}
});
app.listen(3000, () => console.log('Webhook listener running'));
Advanced Pattern: Event Sourcing
For complex systems, consider an event-sourcing approach:
class StripeEventStore {
constructor() {
this.events = [];
}
async processEvent(event) {
if (this.hasProcessed(event.id)) return;
// Validate event structure
if (!this.isValidEvent(event)) {
throw new Error('Invalid event structure');
}
// Persist event
await this.persistEvent(event);
// Process in pipeline
await this.applyEvent(event);
// Mark as processed
this.markProcessed(event.id);
}
async applyEvent(event) {
// Implement your event processing pipeline
switch(event.type) {
case 'invoice.paid':
await this.handleInvoicePaid(event);
break;
// Add more cases
}
}
}
Monitoring and Alerting
Implement proper monitoring:
const { createLogger, transports } = require('winston');
const logger = createLogger({
transports: [
new transports.Console(),
new transports.File({ filename: 'webhook.log' })
]
});
// Instrument the webhook handler
app.post('/webhook', async (req, res) => {
const start = Date.now();
try {
// ... existing handler code
logger.info('Webhook processed', {
eventId: event.id,
type: event.type,
duration: Date.now() - start
});
} catch (err) {
logger.error('Webhook failed', {
error: err.message,
event: event.id,
stack: err.stack
});
}
});
Conclusion
Building a production-ready Stripe webhook handler requires careful attention to:
- Security through proper signature verification
- Idempotency to handle duplicate events
- Robust error handling and logging
- Scalable architecture for high-volume systems
The implementation shown provides a solid foundation that can be extended with additional event types, retry logic, and integration with your business workflow. Always test webhooks thoroughly using Stripe's test mode before deploying to production.
Remember that webhook processing should generally be fast - if you need to perform long-running operations, consider queuing the event and processing it asynchronously.
🚀 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)