How I Built a Stripe Webhook in Node.js (Full Guide)
Webhooks are essential for modern payment processing systems. Here's my comprehensive guide to building a production-grade Stripe webhook handler in Node.js with proper security, error handling, and idempotency.
Understanding the Architecture
Stripe webhooks operate on a push model - when events occur in Stripe (payment succeeded, charge failed, etc.), Stripe sends HTTP POST requests to your endpoint. The critical components:
- Endpoint Verification - Validating Stripe signatures
- Idempotency - Handling duplicate events
- Error Handling - Proper failure modes
- Event Processing - Business logic execution
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'];
if (!signature) {
throw new Error('Missing stripe-signature header');
}
const secret = process.env.STRIPE_WEBHOOK_SECRET;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
const signatures = signature.split(',')
.map(item => item.split('='))
.reduce((acc, [key, value]) => ({...acc, [key]: value}), {});
if (!crypto.timingSafeEqual(
Buffer.from(signatures['t'], 'utf8'),
Buffer.from(expectedSignature, 'utf8')
)) {
throw new Error('Invalid signature');
}
}
Idempotency Handling
Webhooks may deliver the same event multiple times. Implement idempotency:
const processedEvents = new Set();
async function handleStripeWebhook(req, res) {
try {
verifyStripeSignature(req);
const event = JSON.parse(req.body.toString());
// Check for duplicate events
if (processedEvents.has(event.id)) {
return res.status(200).send(`Already processed event ${event.id}`);
}
processedEvents.add(event.id);
// Process the event
await processEvent(event);
res.status(200).send('Webhook processed');
} catch (err) {
console.error(`Webhook error: ${err.message}`);
res.status(400).send(`Webhook error: ${err.message}`);
}
}
Event Processing
Different event types require different handling:
async function processEvent(event) {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailure(event.data.object);
break;
case 'charge.refunded':
await handleRefund(event.data.object);
break;
// Add more event types as needed
default:
console.log(`Unhandled event type ${event.type}`);
}
}
async function handlePaymentSuccess(paymentIntent) {
// Business logic for successful payment
console.log(`Payment succeeded for ${paymentIntent.amount}`);
// Example: Update database
await db.updateOrderStatus(paymentIntent.metadata.orderId, 'paid');
// Example: Send confirmation email
await emailService.sendReceipt(paymentIntent.customer_email, {
amount: paymentIntent.amount / 100,
currency: paymentIntent.currency
});
}
Error Handling and Retries
Implement proper error handling with exponential backoff:
async function processWithRetries(event, maxRetries = 3) {
let attempts = 0;
while (attempts < maxRetries) {
try {
await processEvent(event);
return;
} catch (err) {
attempts++;
const delay = Math.pow(2, attempts) * 1000;
if (attempts >= maxRetries) {
throw err;
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Testing Webhooks Locally
Use the Stripe CLI for local testing:
stripe listen --forward-to localhost:4242/webhook
Then trigger test events:
stripe trigger payment_intent.succeeded
Production Considerations
For production deployment:
- HTTPS - Mandatory for webhooks
- Queue Processing - Offload heavy processing
- Logging - Comprehensive event logging
- Monitoring - Alert on failures
Example with queue:
const { Worker } = require('bullmq');
// In your webhook handler
queue.add('process-stripe-event', { event });
// Worker setup
const worker = new Worker('process-stripe-event', async job => {
await processEvent(job.data.event);
}, { connection: redisClient });
Complete Implementation
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 { Worker } = require('bullmq');
const app = express();
const processedEvents = new Set();
// Middleware
app.post('/webhook',
bodyParser.raw({type: 'application/json'}),
handleStripeWebhook
);
// Webhook handler
async function handleStripeWebhook(req, res) {
try {
verifyStripeSignature(req);
const event = JSON.parse(req.body.toString());
if (processedEvents.has(event.id)) {
return res.status(200).end();
}
processedEvents.add(event.id);
await processWithRetries(event);
res.status(200).end();
} catch (err) {
console.error(`Webhook error: ${err.message}`);
res.status(400).send(`Webhook error: ${err.message}`);
}
}
// Verification
function verifyStripeSignature(req) {
const signature = req.headers['stripe-signature'];
if (!signature) throw new Error('Missing signature');
const secret = process.env.STRIPE_WEBHOOK_SECRET;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
const sigs = signature.split(',')
.map(item => item.split('='))
.reduce((acc, [k, v]) => ({...acc, [k]: v}), {});
if (!crypto.timingSafeEqual(
Buffer.from(sigs['t'], 'utf8'),
Buffer.from(expectedSig, 'utf8')
)) {
throw new Error('Invalid signature');
}
}
// Event processing
async function processEvent(event) {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
// Add other cases
default:
console.log(`Unhandled event: ${event.type}`);
}
}
// Start server
app.listen(4242, () => console.log('Webhook ready'));
Best Practices
- Secret Rotation - Regularly rotate webhook secrets
- Idempotency Keys - Use Stripe's idempotency keys for API calls
- Timeouts - Configure appropriate timeout values
- DB Transactions - Use transactions for database updates
- Circuit Breakers - Implement for dependent services
This implementation provides a solid foundation for handling Stripe webhooks in production Node.js applications. The key aspects are security verification, proper error handling, and idempotent processing to ensure reliable operation.
🚀 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)