Loading...
Loading...
Building reliable webhook handlers requires more than just parsing JSON. Learn the best practices that prevent production issues and make your integrations robust.
Webhook providers often send the same event multiple times. Network issues, timeouts, or retry logic can cause duplicates. Your handler must be idempotent - processing the same event twice should have the same result as processing it once.
// Store processed event IDs
const processedEvents = new Set();
function handleWebhook(event) {
// Check if already processed
if (processedEvents.has(event.id)) {
console.log('Duplicate event, skipping:', event.id);
return { status: 200 }; // Still return 200!
}
// Process the event
processEvent(event);
// Mark as processed
processedEvents.add(event.id);
return { status: 200 };
}Tip: In production, use a database or Redis to track processed event IDs.
Most webhook providers expect a response within 5-30 seconds. If your handler takes too long, the provider will retry, creating duplicate events. Always acknowledge receipt immediately and process heavy logic asynchronously.
✓ Do
✗ Don't
Never trust incoming webhooks blindly. Attackers can forge requests to your endpoint. Most providers include a signature header that you must verify using your secret key.
// Example: Verifying a Stripe webhook signature
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
function handleStripeWebhook(req) {
const signature = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
const event = stripe.webhooks.constructEvent(
req.body,
signature,
webhookSecret
);
// Signature valid, process event
return processEvent(event);
} catch (err) {
// Signature invalid, reject request
return { status: 400, error: 'Invalid signature' };
}
}Don't just test the happy path. Webhook payloads can vary significantly based on the event type, account configuration, or API version. Test these scenarios:
Your response status code tells the webhook provider whether to retry the delivery. Use the correct codes to prevent unnecessary retries or missed events.
| Status | Meaning | Provider Action |
|---|---|---|
| 200 | Success | No retry |
| 400 | Bad request | No retry (your fault) |
| 500 | Server error | Will retry |
| 503 | Temporarily unavailable | Will retry later |
When something goes wrong in production, you'll need logs to debug. Log the raw payload, headers, and your processing decisions. Use structured logging for easier searching.
// Log at key points
console.log({
type: 'webhook_received',
eventId: event.id,
eventType: event.type,
timestamp: new Date().toISOString(),
headers: sanitizeHeaders(req.headers),
});
// After processing
console.log({
type: 'webhook_processed',
eventId: event.id,
result: 'success',
processingTimeMs: Date.now() - startTime,
});