Webhooks
Receiving Webhooks
Express (with signature verification)
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['stripe-signature']!; const event = stripe.webhooks.constructEvent(req.body, signature, WEBHOOK_SECRET);
// Idempotency: check if already processed const existing = await db.webhookEvent.findUnique({ where: { eventId: event.id } }); if (existing) return res.json({ received: true });
// Process switch (event.type) { case 'checkout.session.completed': await fulfillOrder(event.data.object); break; }
// Mark as processed await db.webhookEvent.create({ data: { eventId: event.id, type: event.type } }); res.json({ received: true }); });
Generic HMAC Verification
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(payload).digest('hex');
return timingSafeEqual(Buffer.from(signature), Buffer.from(sha256=${expected}));
}
app.post('/webhooks/github', express.raw({ type: '/' }), (req, res) => { const sig = req.headers['x-hub-signature-256'] as string; if (!verifyWebhookSignature(req.body.toString(), sig, GITHUB_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Process event... res.status(200).json({ received: true }); });
Sending Webhooks
class WebhookDispatcher { async dispatch(url: string, event: WebhookEvent, secret: string) { const payload = JSON.stringify(event); const signature = createHmac('sha256', secret).update(payload).digest('hex');
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': `sha256=${signature}`,
'X-Webhook-Id': event.id,
'X-Webhook-Timestamp': new Date().toISOString(),
},
body: payload,
signal: AbortSignal.timeout(10000),
});
if (!response.ok) throw new WebhookDeliveryError(response.status);
} }
Retry with Exponential Backoff
// Use job queue for reliable delivery await webhookQueue.add('deliver', { url: subscription.url, event: { id: uuid(), type: 'order.created', data: order }, secret: subscription.secret, }, { attempts: 5, backoff: { type: 'exponential', delay: 60000 }, // 1m, 2m, 4m, 8m, 16m });
Webhook Event Schema
interface WebhookEvent { id: string; // Unique event ID (for idempotency) type: string; // 'order.created', 'user.deleted' timestamp: string; // ISO 8601 data: unknown; // Event payload version: string; // API version }
Anti-Patterns
Anti-Pattern Fix
No signature verification Always verify HMAC signature
Processing before responding 200 Respond 200 immediately, process async
No idempotency check Store processed event IDs
Using parsed body for verification Use raw body for signature check
No retry on send failures Use job queue with exponential backoff
Synchronous webhook delivery Dispatch via background job queue
Production Checklist
-
HMAC signature verification on receive
-
Raw body parsing (not JSON-parsed) for signature
-
Idempotency: deduplicate by event ID
-
Respond 200 before processing
-
Retry with exponential backoff on send
-
Dead letter queue for failed deliveries
-
Webhook event log for debugging