34 KiB
description, shortcut
| description | shortcut |
|---|---|
| Create secure webhook endpoints with validation and resilience | webhook |
Create Webhook Handler
Automatically generate production-ready webhook endpoints with signature verification, idempotency, retry handling, event processing, and comprehensive security measures for reliable third-party integrations.
When to Use This Command
Use /create-webhook-handler when you need to:
- Receive real-time events from external services (Stripe, GitHub, Slack)
- Build event-driven architectures with external triggers
- Implement payment processing notifications
- Handle CI/CD pipeline events
- Process incoming data from IoT devices
- Create serverless event receivers
DON'T use this when:
- Building internal service communication (use message queues)
- Need guaranteed ordering (webhooks are best-effort)
- Handling large payloads (>10MB typically)
- Bidirectional communication needed (consider WebSockets)
Design Decisions
This command implements signature-based verification as the primary approach because:
- Industry-standard security practice (HMAC-SHA256)
- Prevents replay attacks with timestamp validation
- Ensures message integrity and authenticity
- Widely supported by webhook providers
- Simple to implement and verify
- Minimal overhead for validation
Alternative considered: OAuth token validation
- More complex setup required
- Additional network calls for validation
- Higher latency
- Recommended for user-authenticated webhooks
Alternative considered: mTLS (mutual TLS)
- Strongest security option
- Complex certificate management
- Not widely supported
- Recommended for enterprise B2B integrations
Prerequisites
Before running this command:
- Webhook URL endpoint defined
- Shared secret or signing key from provider
- Event types to handle documented
- Database for idempotency tracking
- Queue system for async processing (optional)
Implementation Process
Step 1: Create Webhook Endpoint
Define secure POST endpoint with proper routing and middleware.
Step 2: Implement Signature Verification
Validate webhook authenticity using HMAC signatures.
Step 3: Add Idempotency Handling
Prevent duplicate processing of retried events.
Step 4: Route Events to Handlers
Dispatch events to appropriate processing functions.
Step 5: Configure Monitoring
Set up logging, metrics, and alerting for webhook health.
Output Format
The command generates:
webhooks/handlers/- Event-specific handler functionswebhooks/middleware/- Signature verification, rate limitingwebhooks/routes.js- Webhook endpoint definitionswebhooks/processors/- Async event processorswebhooks/schemas/- Event validation schemasconfig/webhooks.json- Provider configurationstests/webhooks/- Integration tests with mock events
Code Examples
Example 1: Multi-Provider Webhook System with Express
// webhooks/middleware/signature.js
const crypto = require('crypto');
const { WebhookError } = require('../errors');
class SignatureVerifier {
constructor(config) {
this.providers = config.providers;
this.timestampTolerance = config.timestampTolerance || 300; // 5 minutes
}
// Stripe signature verification
verifyStripe(payload, signature, secret) {
const elements = signature.split(',');
const timestamp = elements.find(e => e.startsWith('t=')).substring(2);
const signatures = elements
.filter(e => e.startsWith('v1='))
.map(e => e.substring(3));
// Check timestamp to prevent replay attacks
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime - parseInt(timestamp) > this.timestampTolerance) {
throw new WebhookError('Webhook timestamp too old', 'TIMESTAMP_TOO_OLD');
}
// Calculate expected signature
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Compare signatures
const validSignature = signatures.some(sig =>
crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(expectedSignature)
)
);
if (!validSignature) {
throw new WebhookError('Invalid signature', 'INVALID_SIGNATURE');
}
return true;
}
// GitHub signature verification
verifyGitHub(payload, signature, secret) {
if (!signature.startsWith('sha256=')) {
throw new WebhookError('Invalid signature format', 'INVALID_FORMAT');
}
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
if (!isValid) {
throw new WebhookError('Invalid signature', 'INVALID_SIGNATURE');
}
return true;
}
// Shopify signature verification
verifyShopify(payload, signature, secret) {
const hash = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('base64');
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(hash)
);
if (!isValid) {
throw new WebhookError('Invalid signature', 'INVALID_SIGNATURE');
}
return true;
}
// Generic HMAC verification
verifyHMAC(payload, signature, secret, algorithm = 'sha256') {
const expectedSignature = crypto
.createHmac(algorithm, secret)
.update(payload)
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
if (!isValid) {
throw new WebhookError('Invalid signature', 'INVALID_SIGNATURE');
}
return true;
}
// Middleware factory
createMiddleware(provider) {
return async (req, res, next) => {
try {
const config = this.providers[provider];
if (!config) {
throw new WebhookError('Unknown provider', 'UNKNOWN_PROVIDER');
}
const rawBody = req.rawBody || JSON.stringify(req.body);
const signature = req.headers[config.headerName];
if (!signature) {
throw new WebhookError('Missing signature', 'MISSING_SIGNATURE');
}
// Verify based on provider
switch (provider) {
case 'stripe':
this.verifyStripe(rawBody, signature, config.secret);
break;
case 'github':
this.verifyGitHub(rawBody, signature, config.secret);
break;
case 'shopify':
this.verifyShopify(rawBody, signature, config.secret);
break;
default:
this.verifyHMAC(rawBody, signature, config.secret, config.algorithm);
}
// Add provider info to request
req.webhookProvider = provider;
req.webhookVerified = true;
next();
} catch (error) {
console.error(`Webhook verification failed: ${error.message}`);
res.status(401).json({
error: 'Webhook verification failed',
code: error.code || 'VERIFICATION_FAILED'
});
}
};
}
}
// webhooks/middleware/idempotency.js
class IdempotencyHandler {
constructor(store) {
this.store = store; // Redis or database
this.ttl = 86400; // 24 hours
}
async middleware(req, res, next) {
try {
// Extract idempotency key
const idempotencyKey =
req.headers['idempotency-key'] ||
req.body.id ||
req.body.event_id ||
this.generateKey(req);
// Check if already processed
const existing = await this.store.get(idempotencyKey);
if (existing) {
console.log(`Duplicate webhook detected: ${idempotencyKey}`);
const result = JSON.parse(existing);
return res.status(result.status).json(result.body);
}
// Store the key immediately to prevent race conditions
await this.store.setNX(idempotencyKey, 'PROCESSING', this.ttl);
// Attach key to request
req.idempotencyKey = idempotencyKey;
// Capture response
const originalSend = res.json;
res.json = function(body) {
// Store the response
const response = {
status: res.statusCode,
body: body
};
this.store.set(
idempotencyKey,
JSON.stringify(response),
this.ttl
).catch(err => console.error('Failed to store response:', err));
// Send the original response
return originalSend.call(res, body);
}.bind(this);
next();
} catch (error) {
console.error('Idempotency check failed:', error);
next(); // Continue processing even if idempotency fails
}
}
generateKey(req) {
const content = JSON.stringify({
provider: req.webhookProvider,
body: req.body,
timestamp: Math.floor(Date.now() / 60000) // 1-minute window
});
return crypto
.createHash('sha256')
.update(content)
.digest('hex');
}
}
// webhooks/handlers/stripe.js
const { Queue } = require('bull');
const { EventEmitter } = require('events');
class StripeWebhookHandler extends EventEmitter {
constructor(config) {
super();
this.config = config;
this.queue = new Queue('stripe-events', config.redis);
this.setupProcessors();
}
async handleWebhook(req, res) {
const event = req.body;
// Log webhook receipt
console.log(`Received Stripe webhook: ${event.type} (${event.id})`);
try {
// Validate event data
this.validateEvent(event);
// Queue for async processing
const job = await this.queue.add(event.type, event, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
},
removeOnComplete: false,
removeOnFail: false
});
// Emit event for real-time processing if needed
this.emit(event.type, event);
// Respond quickly to acknowledge receipt
res.status(200).json({
received: true,
jobId: job.id,
eventId: event.id
});
} catch (error) {
console.error('Webhook processing error:', error);
// Still return 200 to prevent retries if it's our error
if (error.code === 'VALIDATION_ERROR') {
res.status(200).json({
received: true,
error: 'Validation failed, event ignored'
});
} else {
// Return 500 for Stripe to retry
res.status(500).json({
error: 'Processing failed',
message: error.message
});
}
}
}
validateEvent(event) {
if (!event.type || !event.id || !event.data) {
throw new Error('Invalid event structure');
}
// Additional validation based on event type
switch (event.type) {
case 'payment_intent.succeeded':
if (!event.data.object.amount) {
throw new Error('Missing amount in payment_intent');
}
break;
case 'customer.subscription.deleted':
if (!event.data.object.customer) {
throw new Error('Missing customer in subscription');
}
break;
}
}
setupProcessors() {
// Payment succeeded
this.queue.process('payment_intent.succeeded', async (job) => {
const event = job.data;
const payment = event.data.object;
// Update order status
await this.updateOrderStatus(payment.metadata.orderId, 'paid');
// Send confirmation email
await this.sendPaymentConfirmation(payment);
// Update inventory
await this.updateInventory(payment.metadata.orderId);
// Analytics
await this.trackRevenue(payment.amount / 100, payment.currency);
return { processed: true, orderId: payment.metadata.orderId };
});
// Subscription canceled
this.queue.process('customer.subscription.deleted', async (job) => {
const event = job.data;
const subscription = event.data.object;
// Update user account
await this.updateUserSubscription(subscription.customer, null);
// Send cancellation email
await this.sendCancellationEmail(subscription);
// Clean up resources
await this.cleanupUserResources(subscription.customer);
return { processed: true, customerId: subscription.customer };
});
// Payment failed
this.queue.process('payment_intent.payment_failed', async (job) => {
const event = job.data;
const payment = event.data.object;
// Notify customer
await this.sendPaymentFailedNotification(payment);
// Update order status
await this.updateOrderStatus(payment.metadata.orderId, 'payment_failed');
// Alert operations team if high-value
if (payment.amount > 100000) { // $1000+
await this.alertOperationsTeam(payment);
}
return { processed: true, orderId: payment.metadata.orderId };
});
// Handle job failures
this.queue.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err);
this.alertOnFailure(job, err);
});
// Monitor job completion
this.queue.on('completed', (job, result) => {
console.log(`Job ${job.id} completed:`, result);
});
}
// Helper methods
async updateOrderStatus(orderId, status) {
// Implementation
}
async sendPaymentConfirmation(payment) {
// Implementation
}
async updateInventory(orderId) {
// Implementation
}
async trackRevenue(amount, currency) {
// Implementation
}
async alertOnFailure(job, error) {
// Send to monitoring service
}
}
// webhooks/routes.js
const express = require('express');
const router = express.Router();
const { SignatureVerifier } = require('./middleware/signature');
const { IdempotencyHandler } = require('./middleware/idempotency');
const { RateLimiter } = require('./middleware/rateLimiter');
const { StripeWebhookHandler } = require('./handlers/stripe');
const { GitHubWebhookHandler } = require('./handlers/github');
const Redis = require('ioredis');
// Initialize middleware
const redis = new Redis(process.env.REDIS_URL);
const signatureVerifier = new SignatureVerifier({
providers: {
stripe: {
secret: process.env.STRIPE_WEBHOOK_SECRET,
headerName: 'stripe-signature'
},
github: {
secret: process.env.GITHUB_WEBHOOK_SECRET,
headerName: 'x-hub-signature-256'
},
shopify: {
secret: process.env.SHOPIFY_WEBHOOK_SECRET,
headerName: 'x-shopify-hmac-sha256'
}
},
timestampTolerance: 300
});
const idempotencyHandler = new IdempotencyHandler(redis);
const rateLimiter = new RateLimiter({
windowMs: 60000,
maxRequests: 100,
keyGenerator: (req) => req.webhookProvider
});
// Initialize handlers
const stripeHandler = new StripeWebhookHandler({ redis });
const githubHandler = new GitHubWebhookHandler({ redis });
// Stripe webhooks
router.post('/stripe',
express.raw({ type: 'application/json' }),
signatureVerifier.createMiddleware('stripe'),
idempotencyHandler.middleware.bind(idempotencyHandler),
rateLimiter.middleware(),
stripeHandler.handleWebhook.bind(stripeHandler)
);
// GitHub webhooks
router.post('/github',
express.json({ limit: '10mb' }),
signatureVerifier.createMiddleware('github'),
idempotencyHandler.middleware.bind(idempotencyHandler),
rateLimiter.middleware(),
githubHandler.handleWebhook.bind(githubHandler)
);
// Health check endpoint
router.get('/health', (req, res) => {
res.json({
status: 'healthy',
providers: ['stripe', 'github', 'shopify'],
timestamp: new Date().toISOString()
});
});
module.exports = router;
Example 2: Serverless Webhook Handler with AWS Lambda
# webhook_handler.py - AWS Lambda function
import json
import hmac
import hashlib
import time
import boto3
from typing import Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum
dynamodb = boto3.resource('dynamodb')
sqs = boto3.client('sqs')
sns = boto3.client('sns')
class WebhookProvider(Enum):
STRIPE = "stripe"
GITHUB = "github"
SLACK = "slack"
SENDGRID = "sendgrid"
@dataclass
class WebhookConfig:
provider: WebhookProvider
secret: str
header_name: str
algorithm: str = "sha256"
timestamp_tolerance: int = 300
class WebhookValidator:
"""Validates webhook signatures from various providers."""
@staticmethod
def validate_stripe(body: str, signature: str, secret: str) -> bool:
"""Validate Stripe webhook signature."""
elements = signature.split(',')
timestamp = None
signatures = []
for element in elements:
key, value = element.split('=')
if key == 't':
timestamp = value
elif key == 'v1':
signatures.append(value)
if not timestamp:
raise ValueError("No timestamp in signature")
# Check timestamp
current_time = int(time.time())
if current_time - int(timestamp) > 300: # 5 minutes
raise ValueError("Timestamp too old")
# Calculate expected signature
signed_payload = f"{timestamp}.{body}"
expected_sig = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Compare signatures
return any(
hmac.compare_digest(sig, expected_sig)
for sig in signatures
)
@staticmethod
def validate_github(body: str, signature: str, secret: str) -> bool:
"""Validate GitHub webhook signature."""
if not signature.startswith('sha256='):
raise ValueError("Invalid signature format")
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
body.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@staticmethod
def validate_generic(
body: str,
signature: str,
secret: str,
algorithm: str = "sha256"
) -> bool:
"""Generic HMAC validation."""
hash_func = getattr(hashlib, algorithm)
expected = hmac.new(
secret.encode('utf-8'),
body.encode('utf-8'),
hash_func
).hexdigest()
return hmac.compare_digest(signature, expected)
class IdempotencyManager:
"""Manages idempotent processing of webhooks."""
def __init__(self, table_name: str):
self.table = dynamodb.Table(table_name)
async def check_and_record(self, event_id: str) -> bool:
"""Check if event was already processed and record it."""
try:
# Try to create item with conditional check
self.table.put_item(
Item={
'event_id': event_id,
'processed_at': int(time.time()),
'ttl': int(time.time()) + 86400 # 24 hour TTL
},
ConditionExpression='attribute_not_exists(event_id)'
)
return False # Not processed before
except Exception as e:
if 'ConditionalCheckFailedException' in str(e):
return True # Already processed
raise
class WebhookProcessor:
"""Processes webhook events asynchronously."""
def __init__(self, queue_url: str, topic_arn: str):
self.queue_url = queue_url
self.topic_arn = topic_arn
async def process_stripe_event(self, event: Dict[str, Any]) -> Dict[str, Any]:
"""Process Stripe webhook events."""
event_type = event.get('type')
event_data = event.get('data', {}).get('object', {})
handlers = {
'payment_intent.succeeded': self.handle_payment_success,
'payment_intent.failed': self.handle_payment_failure,
'customer.subscription.created': self.handle_subscription_created,
'customer.subscription.deleted': self.handle_subscription_deleted,
'invoice.payment_failed': self.handle_invoice_failed
}
handler = handlers.get(event_type)
if handler:
return await handler(event_data)
else:
print(f"No handler for event type: {event_type}")
return {'processed': False, 'reason': 'No handler'}
async def handle_payment_success(self, payment: Dict[str, Any]) -> Dict[str, Any]:
"""Handle successful payment."""
# Queue order fulfillment
await self.queue_message({
'action': 'fulfill_order',
'order_id': payment.get('metadata', {}).get('order_id'),
'amount': payment.get('amount'),
'currency': payment.get('currency')
})
# Send notification
await self.send_notification({
'type': 'payment_success',
'customer_email': payment.get('receipt_email'),
'amount': payment.get('amount') / 100
})
return {'processed': True, 'action': 'payment_processed'}
async def queue_message(self, message: Dict[str, Any]):
"""Queue message for async processing."""
sqs.send_message(
QueueUrl=self.queue_url,
MessageBody=json.dumps(message),
MessageAttributes={
'Type': {
'StringValue': message.get('action', 'unknown'),
'DataType': 'String'
}
}
)
async def send_notification(self, notification: Dict[str, Any]):
"""Send SNS notification."""
sns.publish(
TopicArn=self.topic_arn,
Message=json.dumps(notification),
Subject=f"Webhook Event: {notification.get('type')}"
)
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""AWS Lambda handler for webhook processing."""
# Parse request
body = event.get('body', '')
headers = event.get('headers', {})
path = event.get('path', '')
# Determine provider from path
provider = path.split('/')[-1] # /webhooks/stripe -> stripe
try:
# Get configuration
config = WebhookConfig(
provider=WebhookProvider(provider),
secret=os.environ[f'{provider.upper()}_WEBHOOK_SECRET'],
header_name=headers.get('x-webhook-header', 'signature')
)
# Validate signature
validator = WebhookValidator()
signature = headers.get(config.header_name)
if not signature:
return {
'statusCode': 401,
'body': json.dumps({'error': 'Missing signature'})
}
is_valid = False
if config.provider == WebhookProvider.STRIPE:
is_valid = validator.validate_stripe(body, signature, config.secret)
elif config.provider == WebhookProvider.GITHUB:
is_valid = validator.validate_github(body, signature, config.secret)
else:
is_valid = validator.validate_generic(
body, signature, config.secret, config.algorithm
)
if not is_valid:
return {
'statusCode': 401,
'body': json.dumps({'error': 'Invalid signature'})
}
# Parse body
webhook_data = json.loads(body)
# Check idempotency
idempotency = IdempotencyManager('webhook-events')
event_id = webhook_data.get('id') or webhook_data.get('event_id')
if await idempotency.check_and_record(event_id):
return {
'statusCode': 200,
'body': json.dumps({'message': 'Already processed'})
}
# Process webhook
processor = WebhookProcessor(
queue_url=os.environ['SQS_QUEUE_URL'],
topic_arn=os.environ['SNS_TOPIC_ARN']
)
if config.provider == WebhookProvider.STRIPE:
result = await processor.process_stripe_event(webhook_data)
else:
# Queue for processing
await processor.queue_message({
'provider': provider,
'event': webhook_data
})
result = {'queued': True}
return {
'statusCode': 200,
'body': json.dumps(result)
}
except Exception as e:
print(f"Webhook processing error: {str(e)}")
# Return 200 to prevent retries for our errors
if 'Validation' in str(e):
return {
'statusCode': 200,
'body': json.dumps({'error': 'Validation failed'})
}
# Return 500 for provider to retry
return {
'statusCode': 500,
'body': json.dumps({'error': 'Processing failed'})
}
Example 3: Testing Webhook Handlers
// tests/webhook.test.js
const request = require('supertest');
const crypto = require('crypto');
const app = require('../app');
const Redis = require('ioredis-mock');
describe('Webhook Handler Tests', () => {
let redis;
beforeEach(() => {
redis = new Redis();
});
describe('Stripe Webhooks', () => {
const secret = 'whsec_test_secret';
function generateStripeSignature(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}`;
}
it('should accept valid webhook with correct signature', async () => {
const payload = JSON.stringify({
id: 'evt_test_123',
type: 'payment_intent.succeeded',
data: {
object: {
id: 'pi_test_123',
amount: 2000,
currency: 'usd',
metadata: {
order_id: 'order_123'
}
}
}
});
const signature = generateStripeSignature(payload, secret);
const response = await request(app)
.post('/webhooks/stripe')
.set('stripe-signature', signature)
.set('Content-Type', 'application/json')
.send(payload)
.expect(200);
expect(response.body.received).toBe(true);
expect(response.body.eventId).toBe('evt_test_123');
});
it('should reject webhook with invalid signature', async () => {
const payload = JSON.stringify({
id: 'evt_test_123',
type: 'payment_intent.succeeded'
});
await request(app)
.post('/webhooks/stripe')
.set('stripe-signature', 'invalid_signature')
.send(payload)
.expect(401);
});
it('should handle idempotent requests', async () => {
const payload = JSON.stringify({
id: 'evt_test_duplicate',
type: 'payment_intent.succeeded',
data: { object: { amount: 1000 } }
});
const signature = generateStripeSignature(payload, secret);
// First request
const response1 = await request(app)
.post('/webhooks/stripe')
.set('stripe-signature', signature)
.send(payload)
.expect(200);
// Duplicate request
const response2 = await request(app)
.post('/webhooks/stripe')
.set('stripe-signature', signature)
.send(payload)
.expect(200);
// Both should return same response
expect(response1.body).toEqual(response2.body);
});
it('should reject old timestamps', async () => {
const oldTimestamp = Math.floor(Date.now() / 1000) - 400; // 6+ minutes old
const payload = JSON.stringify({
id: 'evt_old',
type: 'payment_intent.succeeded'
});
const signedPayload = `${oldTimestamp}.${payload}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
const signatureHeader = `t=${oldTimestamp},v1=${signature}`;
await request(app)
.post('/webhooks/stripe')
.set('stripe-signature', signatureHeader)
.send(payload)
.expect(401);
});
});
describe('GitHub Webhooks', () => {
const secret = 'github_secret';
function generateGitHubSignature(payload, secret) {
return 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
}
it('should process push event', async () => {
const payload = JSON.stringify({
ref: 'refs/heads/main',
repository: {
name: 'test-repo',
full_name: 'user/test-repo'
},
commits: [{
id: 'abc123',
message: 'Test commit'
}]
});
const signature = generateGitHubSignature(payload, secret);
const response = await request(app)
.post('/webhooks/github')
.set('x-hub-signature-256', signature)
.set('x-github-event', 'push')
.send(payload)
.expect(200);
expect(response.body.received).toBe(true);
});
});
describe('Rate Limiting', () => {
it('should rate limit excessive requests', async () => {
const payload = JSON.stringify({
id: 'evt_rate_test',
type: 'test.event'
});
const signature = generateStripeSignature(payload, 'test_secret');
// Make many requests
const requests = [];
for (let i = 0; i < 150; i++) {
requests.push(
request(app)
.post('/webhooks/stripe')
.set('stripe-signature', signature)
.send(payload)
);
}
const responses = await Promise.all(requests);
const rateLimited = responses.filter(r => r.status === 429);
expect(rateLimited.length).toBeGreaterThan(0);
});
});
});
Error Handling
| Error | Cause | Solution |
|---|---|---|
| "Invalid signature" | Wrong secret or tampered payload | Verify secret key configuration |
| "Timestamp too old" | Webhook delivered late | Increase tolerance or check delays |
| "Duplicate event" | Webhook retried by provider | Idempotency working correctly |
| "Processing timeout" | Long-running handler | Move to async queue processing |
| "Rate limit exceeded" | Too many webhooks | Increase limits or optimize |
Configuration Options
Security Options
signatureAlgorithm: HMAC-SHA256, HMAC-SHA512timestampTolerance: Maximum age of webhook (seconds)ipAllowlist: Restrict to provider IPsrateLimits: Per-provider rate limiting
Processing Options
asyncProcessing: Queue events for background processingretryAttempts: Number of processing retriesdeadLetterQueue: Failed event storageparallelProcessing: Concurrent event handling
Best Practices
DO:
- Always verify signatures before processing
- Respond quickly (< 5 seconds) to webhook
- Implement idempotency for all handlers
- Log all webhook events for audit
- Use async processing for heavy operations
- Monitor webhook health and success rates
DON'T:
- Process webhooks synchronously if slow
- Trust webhook data without validation
- Ignore timestamp verification
- Return errors for duplicate events
- Skip rate limiting
- Store sensitive webhook data in logs
Performance Considerations
- Use connection pooling for database operations
- Queue heavy processing asynchronously
- Implement circuit breakers for downstream services
- Cache frequently accessed data
- Use bulk operations where possible
- Monitor processing latency and queue depth
Security Considerations
- Always use HTTPS endpoints
- Rotate webhook secrets regularly
- Implement IP allowlisting for critical webhooks
- Never log full webhook payloads with sensitive data
- Use separate secrets per environment
- Monitor for replay attacks
- Implement webhook firewall rules
Related Commands
/api-event-emitter- Create event systems/api-gateway-builder- Build API gateways/message-queue-setup- Configure message queues/serverless-function- Create Lambda handlers/monitoring-setup- Add monitoring
Version History
- v1.0.0 (2024-10): Initial implementation with Stripe, GitHub, Shopify support
- Planned v1.1.0: Add Twilio, SendGrid, and Slack webhook handlers