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 walk through building a production-grade Stripe webhook handler in Node.js that handles events securely and efficiently.
Webhook Architecture Fundamentals
Stripe webhooks use HTTP POST requests to notify your server about events. The critical components we need:
- Endpoint Verification: Verify requests originate from Stripe
- Event Processing: Handle different event types appropriately
- Idempotency: Prevent duplicate processing
- Error Handling: Graceful failure and retry logic
Step 1: Project Setup
First, initialize a Node.js project and install dependencies:
npm init -y
npm install express stripe crypto body-parser
Step 2: Webhook Endpoint Implementation
Create webhook.js with this foundation:
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();
// Middleware to verify raw body for signature validation
app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (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 event
await handleWebhookEvent(event);
res.json({received: true});
});
Step 3: Event Processing Logic
Implement the event handler with proper type checking:
const eventHandlers = {
'payment_intent.succeeded': handlePaymentSuccess,
'payment_intent.payment_failed': handlePaymentFailure,
'charge.refunded': handleRefund,
'customer.subscription.deleted': handleSubscriptionCancel
};
async function handleWebhookEvent(event) {
const handler = eventHandlers[event.type];
if (handler) {
try {
await handler(event.data.object);
} catch (err) {
console.error(`Error processing ${event.type}:`, err);
throw err; // Will trigger Stripe retry
}
} else {
console.log(`Unhandled event type: ${event.type}`);
}
}
Step 4: Implementing Event Handlers
Here's how to implement specific handlers:
async function handlePaymentSuccess(paymentIntent) {
// Verify idempotency
if (await checkIfProcessed(paymentIntent.id)) {
console.log(`Payment ${paymentIntent.id} already processed`);
return;
}
// Business logic
await fulfillOrder(paymentIntent.metadata.orderId);
await markAsProcessed(paymentIntent.id);
console.log(`Successfully processed payment ${paymentIntent.id}`);
}
async function handleSubscriptionCancel(subscription) {
await revokeAccess(subscription.customer);
await sendCancellationEmail(subscription.customer);
console.log(`Processed cancellation for ${subscription.id}`);
}
Step 5: Idempotency Implementation
Prevent duplicate processing with Redis:
const redis = require('redis');
const client = redis.createClient(process.env.REDIS_URL);
async function checkIfProcessed(eventId) {
return new Promise((resolve) => {
client.exists(`processed:${eventId}`, (err, reply) => {
resolve(reply === 1);
});
});
}
async function markAsProcessed(eventId) {
// Store for 72 hours (Stripe's retry window)
client.setex(`processed:${eventId}`, 72 * 3600, '1');
}
Step 6: Advanced Error Handling
Implement exponential backoff for retries:
async function withRetry(fn, maxRetries = 3, attempt = 1) {
try {
return await fn();
} catch (err) {
if (attempt >= maxRetries) throw err;
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return withRetry(fn, maxRetries, attempt + 1);
}
}
// Usage in handler:
await withRetry(() => fulfillOrder(orderId));
Step 7: Testing Webhooks Locally
Use the Stripe CLI for local testing:
stripe listen --forward-to localhost:3000/webhook
stripe trigger payment_intent.succeeded
Step 8: Production Considerations
- Rate Limiting: Protect your endpoint
- Queue Processing: Use Bull or similar for heavy tasks
- Monitoring: Track event processing times and failures
// Rate limiting example
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100 // Stripe recommends handling at least 100 req/min
});
app.post('/webhook', limiter, bodyParser.raw(...));
Complete Webhook Class
Here's a production-ready implementation:
class StripeWebhook {
constructor(options = {}) {
this.stripe = require('stripe')(options.apiKey);
this.redis = require('redis').createClient(options.redisUrl);
this.secret = options.webhookSecret;
this.handlers = new Map();
}
addHandler(eventType, handler) {
this.handlers.set(eventType, handler);
}
async processEvent(rawBody, signature) {
const event = this.stripe.webhooks.constructEvent(
rawBody,
signature,
this.secret
);
if (await this.isDuplicate(event.id)) {
return { status: 'already_processed' };
}
const handler = this.handlers.get(event.type);
if (!handler) {
return { status: 'unhandled_event_type' };
}
try {
await handler(event.data.object);
await this.markAsProcessed(event.id);
return { status: 'success' };
} catch (err) {
console.error(`Event processing failed: ${err}`);
throw err;
}
}
async isDuplicate(eventId) {
return new Promise((resolve) => {
this.redis.exists(`event:${eventId}`, (err, reply) => {
resolve(reply === 1);
});
});
}
async markAsProcessed(eventId) {
this.redis.setex(`event:${eventId}`, 72 * 3600, '1');
}
}
Deployment Best Practices
- HTTPS: Always use HTTPS in production
- Secret Rotation: Regularly rotate webhook secrets
- IP Allowlisting: Restrict to Stripe's IP ranges
- Logging: Comprehensive event logging
// IP allowlisting middleware
const stripeIps = ['54.187.174.169', '54.187.205.235', /* ... */];
function stripeIpWhitelist(req, res, next) {
const ip = req.ip.replace('::ffff:', '');
if (!stripeIps.includes(ip)) {
return res.status(403).send('Forbidden');
}
next();
}
app.post('/webhook', stripeIpWhitelist, ...);
Monitoring and Alerting
Implement Prometheus metrics for observability:
const client = require('prom-client');
const webhookRequests = new client.Counter({
name: 'stripe_webhook_requests_total',
help: 'Total Stripe webhook requests',
labelNames: ['type', 'status']
});
// In your handler:
webhookRequests.inc({ type: event.type, status: 'success' });
Conclusion
This implementation provides:
- Secure endpoint verification
- Proper event handling
- Idempotent processing
- Error recovery
- Production-grade reliability
The complete code is available on GitHub (link would go here). Remember to thoroughly test your webhook handler before deploying to production.
🚀 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)