inutes
function verifyTimestamp(sig) {
const [, timestamp] = sig.split(',')[0].split('=');
const timeDiff = Math.floor(Date.now() / 1000) - parseInt(timestamp);
return timeDiff <= MAX_TOLERANCE_SECONDS;
}
Enter fullscreen mode Exit fullscreen mode
### [](#secret-rotation)Secret Rotation
Implement webhook secret rotation:
async function getWebhookSecret(version = 'latest') {
// In production, fetch from secure storage
const secrets = {
'v1': process.env.STRIPE_WEBHOOK_SECRET_V1,
'v2': process.env.STRIPE_WEBHOOK_SECRET_V2,
'latest': process.env.STRIPE_WEBHOOK_SECRET
};
return secrets[version];
}
// Modified verification:
try {
const secret = await getWebhookSecret();
event = stripe.webhooks.constructEvent(req.body, sig, secret);
} catch (err) {
// Try with previous version if latest fails
try {
const secret = await getWebhookSecret('v1');
event = stripe.webhooks.constructEvent(req.body, sig, secret);
} catch (err2) {
throw err;
}
}
Enter fullscreen mode Exit fullscreen mode
## [](#event-processing-architecture)Event Processing Architecture
For production workloads, you'll want to:
1. Validate the event
2. Push to a queue
3. Process asynchronously
Here's a Redis-based queue implementation:
const { createClient } = require('redis');
const redisClient = createClient({ url: process.env.REDIS_URL });
async function enqueueStripeEvent(event) {
await redisClient.connect();
try {
await redisClient.lPush(
'stripe_events',
JSON.stringify({
id: event.id,
type: event.type,
data: event.data,
created: event.created
})
);
} finally {
await redisClient.disconnect();
}
}
// Worker process example
async function processStripeEvents() {
await redisClient.connect();
while (true) {
const eventStr = await redisClient.rPop('stripe_events');
if (!eventStr) {
await new Promise(resolve => setTimeout(resolve, 1000));
continue;
}
const event = JSON.parse(eventStr);
try {
await handleEvent(event);
} catch (err) {
console.error(`Failed processing event ${event.id}:`, err);
// Implement retry logic here
}
}
}
Enter fullscreen mode Exit fullscreen mode
## [](#idempotency-handling)Idempotency Handling
Stripe events can be delivered multiple times. Implement idempotency:
const { MongoClient } = require('mongodb');
const client = new MongoClient(process.env.MONGODB_URI);
async function ensureIdempotency(eventId, handler) {
await client.connect();
const db = client.db('stripe_webhooks');
const collection = db.collection('processed_events');
const existing = await collection.findOne({ eventId });
if (existing) {
console.log(Event ${eventId} already processed);
return existing.result;
}
try {
const result = await handler();
await collection.insertOne({
eventId,
processedAt: new Date(),
result
});
return result;
} catch (err) {
await collection.insertOne({
eventId,
processedAt: new Date(),
error: err.message
});
throw err;
}
}
// Usage:
await ensureIdempotency(event.id, () => handlePaymentIntentSucceeded(event.data.object));
Enter fullscreen mode Exit fullscreen mode
## [](#testing-webhooks-locally)Testing Webhooks Locally
Use the Stripe CLI for local testing:
stripe listen --forward-to localhost:3000/webhook
Enter fullscreen mode Exit fullscreen mode
For automated tests, mock the signature header:
const crypto = require('crypto');
function generateTestSignature(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = ${timestamp}.${payload};
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return t=${timestamp},v1=${signature};
}
Enter fullscreen mode Exit fullscreen mode
## [](#production-deployment-considerations)Production Deployment Considerations
1. **HTTPS**: Mandatory for production webhooks
2. **Rate Limiting**: Protect your endpoint
3. **Scaling**: Use Kubernetes or similar for worker processes
4. **Monitoring**: Track event processing metrics
Example monitoring setup:
const Prometheus = require('prom-client');
const eventCounter = new Prometheus.Counter({
name: 'stripe_events_total',
help: 'Count of Stripe webhook events',
labelNames: ['type', 'status']
});
// In your handler:
eventCounter.inc({ type: event.type, status: 'received' });
try {
await handleEvent(event);
eventCounter.inc({ type: event.type, status: 'processed' });
} catch (err) {
eventCounter.inc({ type: event.type, status: 'failed' });
throw err;
}
Enter fullscreen mode Exit fullscreen mode
## [](#complete-production-example)Complete Production Example
Here's a consolidated production-ready version:
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const { createClient } = require('redis');
const { MongoClient } = require('mongodb');
const crypto = require('crypto');
const bodyParser = require('body-parser');
const app = express();
const redisClient = createClient({ url: process.env.REDIS_URL });
const mongoClient = new MongoClient(process.env.MONGODB_URI);
// Middleware to verify Stripe signature
async function verifyStripeWebhook(req, res, next) {
const sig = req.headers['stripe-signature'];
if (!sig) return res.status(401).send('No signature provided');
try {
const secret = await getWebhookSecret();
req.stripeEvent = stripe.webhooks.constructEvent(
req.body,
sig,
secret
);
next();
} catch (err) {
console.error('Signature verification failed:', err);
return res.status(401).send('Invalid signature');
}
}
// Main webhook handler
app.post(
'/webhook',
bodyParser.raw({ type: 'application/json' }),
verifyStripeWebhook,
async (req, res) => {
try {
await enqueueStripeEvent(req.stripeEvent);
res.json({ received: true });
} catch (err) {
console.error('Event processing failed:', err);
res.status(500).json({ error: 'Internal server error' });
}
}
);
// Start workers
async function startWorkers(count = 3) {
await redisClient.connect();
await mongoClient.connect();
for (let i = 0; i < count; i++) {
processStripeEvents().catch(err => {
console.error(Worker ${i} failed:, err);
});
}
}
startWorkers();
app.listen(3000, () => {
console.log('Webhook handler listening on port 3000');
});
Enter fullscreen mode Exit fullscreen mode
## [](#final-thoughts)Final Thoughts
Building a robust Stripe webhook handler requires attention to:
1. **Security**: Proper signature verification
2. **Reliability**: Queue processing and idempotency
3. **Observability**: Monitoring and logging
4. **Scalability**: Horizontal scaling for high-volume events
The implementation above addresses all these concerns while maintaining clean, maintainable code. Remember to adjust queue processing and worker counts based on your actual event volume.
For further optimization, consider:
- Event batching for high-volume scenarios
- Dead-letter queues for failed events
- Circuit breakers for downstream service failures
- Canary deployments for webhook handler updates
* * *
### [](#stop-writing-boilerplate-prompts)π 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](https://apolloagmanager.gumroad.com/l/oxlcgd)** ($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](https://apolloagmanager.github.io/apollo-ai-store/) | Or snipe Solana tokens free via [@ApolloSniper\_Bot](https://t.me/ApolloSniper_Bot)._