| name | webhook-integration |
| description | Implement secure webhook systems for event-driven integrations, including signature verification, retry logic, and delivery guarantees. Use when building third-party integrations, event notifications, or real-time data synchronization. |
Webhook Integration
Overview
Implement robust webhook systems for event-driven architectures, enabling real-time communication between services and third-party integrations.
When to Use
- Third-party service integrations (Stripe, GitHub, Shopify)
- Event notification systems
- Real-time data synchronization
- Automated workflow triggers
- Payment processing callbacks
- CI/CD pipeline notifications
- User activity tracking
- Microservices communication
Webhook Architecture
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Event │────────▶│ Webhook │────────▶│ Consumer │
│ Source │ │ Sender │ │ Endpoint │
└──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────────┐
│ Retry Queue │
│ (Failed) │
└──────────────┘
Implementation Examples
1. Webhook Sender (TypeScript)
import crypto from 'crypto';
import axios from 'axios';
interface WebhookEvent {
id: string;
type: string;
timestamp: number;
data: any;
}
interface WebhookEndpoint {
url: string;
secret: string;
events: string[];
active: boolean;
}
interface DeliveryAttempt {
attemptNumber: number;
timestamp: number;
statusCode?: number;
error?: string;
duration: number;
}
class WebhookSender {
private maxRetries = 3;
private retryDelays = [1000, 5000, 30000]; // Exponential backoff
private timeout = 10000; // 10 seconds
/**
* Generate HMAC signature for webhook payload
*/
private generateSignature(
payload: string,
secret: string
): string {
return crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
}
/**
* Send webhook to endpoint
*/
async send(
endpoint: WebhookEndpoint,
event: WebhookEvent
): Promise<DeliveryAttempt[]> {
if (!endpoint.active) {
throw new Error('Endpoint is not active');
}
if (!endpoint.events.includes(event.type)) {
throw new Error(`Event type ${event.type} not subscribed`);
}
const payload = JSON.stringify(event);
const signature = this.generateSignature(payload, endpoint.secret);
const attempts: DeliveryAttempt[] = [];
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
const startTime = Date.now();
try {
const response = await axios.post(endpoint.url, payload, {
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-ID': event.id,
'X-Webhook-Timestamp': event.timestamp.toString(),
'User-Agent': 'WebhookService/1.0'
},
timeout: this.timeout,
validateStatus: (status) => status >= 200 && status < 300
});
const duration = Date.now() - startTime;
attempts.push({
attemptNumber: attempt + 1,
timestamp: Date.now(),
statusCode: response.status,
duration
});
console.log(
`Webhook delivered successfully to ${endpoint.url} (attempt ${attempt + 1})`
);
return attempts;
} catch (error: any) {
const duration = Date.now() - startTime;
attempts.push({
attemptNumber: attempt + 1,
timestamp: Date.now(),
statusCode: error.response?.status,
error: error.message,
duration
});
console.error(
`Webhook delivery failed to ${endpoint.url} (attempt ${attempt + 1}):`,
error.message
);
// Wait before retry (except on last attempt)
if (attempt < this.maxRetries - 1) {
await this.delay(this.retryDelays[attempt]);
}
}
}
throw new Error(
`Webhook delivery failed after ${this.maxRetries} attempts`
);
}
/**
* Batch send webhooks
*/
async sendBatch(
endpoints: WebhookEndpoint[],
event: WebhookEvent
): Promise<Map<string, DeliveryAttempt[]>> {
const results = new Map<string, DeliveryAttempt[]>();
await Promise.allSettled(
endpoints.map(async (endpoint) => {
try {
const attempts = await this.send(endpoint, event);
results.set(endpoint.url, attempts);
} catch (error) {
console.error(`Failed to deliver to ${endpoint.url}:`, error);
}
})
);
return results;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const sender = new WebhookSender();
const endpoint: WebhookEndpoint = {
url: 'https://api.example.com/webhooks',
secret: 'your-webhook-secret',
events: ['user.created', 'user.updated'],
active: true
};
const event: WebhookEvent = {
id: crypto.randomUUID(),
type: 'user.created',
timestamp: Date.now(),
data: {
userId: '123',
email: 'user@example.com'
}
};
await sender.send(endpoint, event);
2. Webhook Receiver (Express)
import express from 'express';
import crypto from 'crypto';
import { body, validationResult } from 'express-validator';
interface WebhookConfig {
secret: string;
signatureHeader: string;
timestampTolerance: number; // seconds
}
class WebhookReceiver {
constructor(private config: WebhookConfig) {}
/**
* Verify webhook signature
*/
verifySignature(
payload: string,
signature: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', this.config.secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
/**
* Verify timestamp to prevent replay attacks
*/
verifyTimestamp(timestamp: number): boolean {
const now = Date.now();
const diff = Math.abs(now - timestamp) / 1000;
return diff <= this.config.timestampTolerance;
}
/**
* Middleware for webhook verification
*/
createMiddleware() {
return async (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
try {
const signature = req.headers[this.config.signatureHeader] as string;
const timestamp = parseInt(
req.headers['x-webhook-timestamp'] as string
);
if (!signature) {
return res.status(401).json({
error: 'Missing signature'
});
}
// Verify timestamp
if (!this.verifyTimestamp(timestamp)) {
return res.status(401).json({
error: 'Invalid timestamp'
});
}
// Get raw body for signature verification
const payload = JSON.stringify(req.body);
// Verify signature
if (!this.verifySignature(payload, signature)) {
return res.status(401).json({
error: 'Invalid signature'
});
}
next();
} catch (error) {
console.error('Webhook verification error:', error);
res.status(500).json({
error: 'Verification failed'
});
}
};
}
}
// Setup Express app
const app = express();
// Use raw body parser for signature verification
app.use(express.json({
verify: (req: any, res, buf) => {
req.rawBody = buf.toString();
}
}));
const receiver = new WebhookReceiver({
secret: process.env.WEBHOOK_SECRET!,
signatureHeader: 'x-webhook-signature',
timestampTolerance: 300 // 5 minutes
});
// Webhook endpoint
app.post('/webhooks',
receiver.createMiddleware(),
[
body('id').isString(),
body('type').isString(),
body('data').isObject()
],
async (req, res) => {
// Validate request
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id, type, data } = req.body;
try {
// Process webhook event
await processWebhookEvent(type, data);
// Respond immediately
res.status(200).json({
received: true,
eventId: id
});
// Process asynchronously if needed
processEventAsync(type, data).catch(console.error);
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({
error: 'Processing failed'
});
}
}
);
async function processWebhookEvent(type: string, data: any): Promise<void> {
switch (type) {
case 'user.created':
await handleUserCreated(data);
break;
case 'payment.success':
await handlePaymentSuccess(data);
break;
default:
console.log(`Unknown event type: ${type}`);
}
}
async function processEventAsync(type: string, data: any): Promise<void> {
// Heavy processing that doesn't need to block the response
}
async function handleUserCreated(data: any): Promise<void> {
console.log('User created:', data);
}
async function handlePaymentSuccess(data: any): Promise<void> {
console.log('Payment successful:', data);
}
app.listen(3000, () => {
console.log('Webhook receiver listening on port 3000');
});
3. Webhook Queue with Bull
import Queue from 'bull';
import axios from 'axios';
interface WebhookJob {
endpoint: WebhookEndpoint;
event: WebhookEvent;
}
class WebhookQueue {
private queue: Queue.Queue<WebhookJob>;
constructor(redisUrl: string) {
this.queue = new Queue('webhooks', redisUrl, {
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
},
removeOnComplete: 100,
removeOnFail: 1000
}
});
this.setupProcessors();
this.setupEventHandlers();
}
private setupProcessors(): void {
// Process webhook deliveries
this.queue.process('delivery', 5, async (job) => {
const { endpoint, event } = job.data;
job.log(`Delivering webhook to ${endpoint.url}`);
const sender = new WebhookSender();
const attempts = await sender.send(endpoint, event);
return {
endpoint: endpoint.url,
attempts,
success: true
};
});
}
private setupEventHandlers(): void {
this.queue.on('completed', (job, result) => {
console.log(`Webhook delivered: ${job.id}`, result);
});
this.queue.on('failed', (job, err) => {
console.error(`Webhook delivery failed: ${job?.id}`, err);
});
this.queue.on('stalled', (job) => {
console.warn(`Webhook delivery stalled: ${job.id}`);
});
}
async enqueue(
endpoint: WebhookEndpoint,
event: WebhookEvent,
options?: Queue.JobOptions
): Promise<Queue.Job<WebhookJob>> {
return this.queue.add(
'delivery',
{ endpoint, event },
{
jobId: `${event.id}-${endpoint.url}`,
...options
}
);
}
async enqueueBatch(
endpoints: WebhookEndpoint[],
event: WebhookEvent
): Promise<Queue.Job<WebhookJob>[]> {
const jobs = endpoints.map(endpoint => ({
name: 'delivery',
data: { endpoint, event },
opts: {
jobId: `${event.id}-${endpoint.url}`
}
}));
return this.queue.addBulk(jobs);
}
async getJobStatus(jobId: string): Promise<any> {
const job = await this.queue.getJob(jobId);
if (!job) return null;
return {
id: job.id,
state: await job.getState(),
progress: job.progress(),
attempts: job.attemptsMade,
failedReason: job.failedReason,
finishedOn: job.finishedOn,
processedOn: job.processedOn
};
}
async retryFailed(jobId: string): Promise<void> {
const job = await this.queue.getJob(jobId);
if (!job) {
throw new Error('Job not found');
}
await job.retry();
}
async pause(): Promise<void> {
await this.queue.pause();
}
async resume(): Promise<void> {
await this.queue.resume();
}
async close(): Promise<void> {
await this.queue.close();
}
}
// Usage
const webhookQueue = new WebhookQueue('redis://localhost:6379');
// Enqueue single webhook
await webhookQueue.enqueue(endpoint, event, {
delay: 1000, // Delay 1 second
priority: 1
});
// Enqueue to multiple endpoints
await webhookQueue.enqueueBatch(endpoints, event);
// Check job status
const status = await webhookQueue.getJobStatus('job-id');
console.log('Job status:', status);
4. Webhook Testing Utilities
import express from 'express';
import crypto from 'crypto';
class WebhookTester {
private app: express.Application;
private receivedEvents: WebhookEvent[] = [];
constructor() {
this.app = express();
this.setupTestEndpoint();
}
private setupTestEndpoint(): void {
this.app.use(express.json());
this.app.post('/test-webhook', (req, res) => {
const event = req.body;
// Validate signature if provided
const signature = req.headers['x-webhook-signature'] as string;
if (signature) {
// Verify signature here
}
// Store received event
this.receivedEvents.push(event);
console.log('Received webhook:', event);
// Respond based on test scenario
res.status(200).json({
received: true,
eventId: event.id
});
});
// Endpoint that simulates failures
this.app.post('/test-webhook/fail', (req, res) => {
const failureType = req.query.type;
switch (failureType) {
case 'timeout':
// Don't respond (simulates timeout)
break;
case 'server-error':
res.status(500).json({ error: 'Internal server error' });
break;
case 'unauthorized':
res.status(401).json({ error: 'Unauthorized' });
break;
default:
res.status(400).json({ error: 'Bad request' });
}
});
}
start(port: number): void {
this.app.listen(port, () => {
console.log(`Webhook test server running on port ${port}`);
});
}
getReceivedEvents(): WebhookEvent[] {
return this.receivedEvents;
}
clearEvents(): void {
this.receivedEvents = [];
}
/**
* Create mock webhook event
*/
static createMockEvent(type: string, data: any): WebhookEvent {
return {
id: crypto.randomUUID(),
type,
timestamp: Date.now(),
data
};
}
}
// Testing
const tester = new WebhookTester();
tester.start(3001);
// Send test webhook
const mockEvent = WebhookTester.createMockEvent('user.created', {
userId: '123',
email: 'test@example.com'
});
const sender = new WebhookSender();
await sender.send(
{
url: 'http://localhost:3001/test-webhook',
secret: 'test-secret',
events: ['user.created'],
active: true
},
mockEvent
);
// Verify received
const received = tester.getReceivedEvents();
console.log('Received events:', received);
Best Practices
✅ DO
- Use HMAC signatures for verification
- Implement idempotency with event IDs
- Return 200 OK quickly, process asynchronously
- Implement exponential backoff for retries
- Include timestamp to prevent replay attacks
- Use queue systems for reliable delivery
- Log all delivery attempts
- Provide webhook testing tools
- Document webhook payload schemas
- Implement webhook management UI
- Allow filtering by event types
- Support webhook versioning
❌ DON'T
- Send sensitive data in webhooks
- Skip signature verification
- Block responses with heavy processing
- Retry indefinitely
- Expose internal error details
- Send webhooks to localhost (in production)
- Forget timeout handling
- Skip rate limiting
Security Checklist
- Verify signatures using HMAC
- Check timestamp to prevent replay attacks
- Validate SSL certificates
- Use HTTPS only
- Implement rate limiting
- Validate webhook URLs
- Rotate secrets periodically
- Log security events
- Implement IP whitelisting (optional)
- Sanitize error messages