# Webhook Implementation **Meta-Description:** Webhooks implementieren für Event-Driven Architecture. Signierung, Retry-Logic und Webhook Management für SaaS. **Keywords:** Webhooks, Event-Driven, Signature Verification, Retry Logic, Webhook Delivery, Integration, API Events --- ## Einführung **Webhooks** ermöglichen Echtzeit-Benachrichtigungen bei Events. Statt Polling erhalten Clients Push-Notifications. Dieser Guide zeigt sichere, zuverlässige Webhook-Implementation mit Retry-Logic und Signaturverifikation. --- ## Webhook Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ WEBHOOK DELIVERY SYSTEM │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Event Flow: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 1. Event occurs in application │ │ │ │ ↓ │ │ │ │ 2. Event published to queue │ │ │ │ ↓ │ │ │ │ 3. Webhook worker picks up event │ │ │ │ ↓ │ │ │ │ 4. Look up subscribed endpoints │ │ │ │ ↓ │ │ │ │ 5. Sign and send payload │ │ │ │ ↓ │ │ │ │ 6. Handle response / retry on failure │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Webhook Payload: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ { │ │ │ │ "id": "evt_123", │ │ │ │ "type": "project.created", │ │ │ │ "timestamp": "2024-01-15T10:30:00Z", │ │ │ │ "data": { │ │ │ │ "id": "proj_456", │ │ │ │ "name": "New Project" │ │ │ │ } │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Security: │ │ ├── HMAC-SHA256 signature in header │ │ ├── Timestamp to prevent replay attacks │ │ ├── HTTPS only │ │ └── IP allowlisting (optional) │ │ │ │ Retry Strategy: │ │ ├── Attempt 1: Immediate │ │ ├── Attempt 2: After 1 minute │ │ ├── Attempt 3: After 5 minutes │ │ ├── Attempt 4: After 30 minutes │ │ └── Attempt 5: After 2 hours │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Webhook Configuration ```typescript // lib/webhooks/types.ts export interface WebhookEndpoint { id: string; tenantId: string; url: string; secret: string; events: string[]; active: boolean; createdAt: Date; metadata?: Record; } export interface WebhookEvent { id: string; type: string; timestamp: Date; data: unknown; tenantId: string; } export interface WebhookDelivery { id: string; endpointId: string; eventId: string; status: 'pending' | 'success' | 'failed'; attempts: number; lastAttemptAt?: Date; nextAttemptAt?: Date; response?: { statusCode: number; body?: string; duration: number; }; } // Event types export const WEBHOOK_EVENTS = { // Project events 'project.created': 'Project was created', 'project.updated': 'Project was updated', 'project.deleted': 'Project was deleted', // User events 'user.created': 'User was created', 'user.updated': 'User was updated', 'user.deleted': 'User was deleted', // Subscription events 'subscription.created': 'Subscription was created', 'subscription.updated': 'Subscription was updated', 'subscription.canceled': 'Subscription was canceled', // Invoice events 'invoice.created': 'Invoice was created', 'invoice.paid': 'Invoice was paid', 'invoice.failed': 'Invoice payment failed' } as const; export type WebhookEventType = keyof typeof WEBHOOK_EVENTS; ``` --- ## Signature & Verification ```typescript // lib/webhooks/signature.ts import { createHmac, timingSafeEqual } from 'crypto'; const SIGNATURE_HEADER = 'X-Webhook-Signature'; const TIMESTAMP_HEADER = 'X-Webhook-Timestamp'; const TOLERANCE_SECONDS = 300; // 5 minutes export function signWebhookPayload( payload: string, secret: string, timestamp: number ): string { const signedPayload = `${timestamp}.${payload}`; const signature = createHmac('sha256', secret) .update(signedPayload) .digest('hex'); return `v1=${signature}`; } export function verifyWebhookSignature( payload: string, signature: string, secret: string, timestamp: number ): boolean { // Check timestamp tolerance const now = Math.floor(Date.now() / 1000); if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) { throw new Error('Webhook timestamp outside tolerance window'); } // Extract signature version and value const [version, receivedSig] = signature.split('='); if (version !== 'v1') { throw new Error('Unsupported signature version'); } // Calculate expected signature const signedPayload = `${timestamp}.${payload}`; const expectedSig = createHmac('sha256', secret) .update(signedPayload) .digest('hex'); // Constant-time comparison const receivedBuffer = Buffer.from(receivedSig, 'hex'); const expectedBuffer = Buffer.from(expectedSig, 'hex'); if (receivedBuffer.length !== expectedBuffer.length) { return false; } return timingSafeEqual(receivedBuffer, expectedBuffer); } // Generate webhook secret export function generateWebhookSecret(): string { return `whsec_${randomBytes(32).toString('hex')}`; } ``` --- ## Event Publishing ```typescript // lib/webhooks/publisher.ts import { db } from '@/lib/db'; import { redis } from '@/lib/redis'; import { WebhookEvent, WebhookEventType } from './types'; import { nanoid } from 'nanoid'; export async function publishWebhookEvent( type: WebhookEventType, tenantId: string, data: unknown ): Promise { const event: WebhookEvent = { id: `evt_${nanoid()}`, type, timestamp: new Date(), data, tenantId }; // Store event await db.webhookEvent.create({ data: { id: event.id, type: event.type, tenantId: event.tenantId, data: event.data as any, createdAt: event.timestamp } }); // Get subscribed endpoints const endpoints = await db.webhookEndpoint.findMany({ where: { tenantId, active: true, events: { has: type } } }); // Create delivery records and queue for (const endpoint of endpoints) { const deliveryId = `del_${nanoid()}`; await db.webhookDelivery.create({ data: { id: deliveryId, endpointId: endpoint.id, eventId: event.id, status: 'pending', attempts: 0, createdAt: new Date() } }); // Add to queue await redis.lpush('webhook:deliveries', JSON.stringify({ deliveryId, endpointId: endpoint.id, eventId: event.id })); } return event.id; } // Helper to publish from anywhere in the app export function webhookEvents(tenantId: string) { return { projectCreated: (project: any) => publishWebhookEvent('project.created', tenantId, project), projectUpdated: (project: any) => publishWebhookEvent('project.updated', tenantId, project), projectDeleted: (projectId: string) => publishWebhookEvent('project.deleted', tenantId, { id: projectId }), userCreated: (user: any) => publishWebhookEvent('user.created', tenantId, user), subscriptionCreated: (subscription: any) => publishWebhookEvent('subscription.created', tenantId, subscription) }; } ``` --- ## Delivery Worker ```typescript // lib/webhooks/worker.ts import { db } from '@/lib/db'; import { redis } from '@/lib/redis'; import { signWebhookPayload } from './signature'; const RETRY_DELAYS = [0, 60, 300, 1800, 7200]; // seconds interface DeliveryJob { deliveryId: string; endpointId: string; eventId: string; } export async function processWebhookDeliveries(): Promise { while (true) { // Get job from queue const jobData = await redis.brpop('webhook:deliveries', 0); if (!jobData) continue; const job: DeliveryJob = JSON.parse(jobData[1]); await deliverWebhook(job); } } async function deliverWebhook(job: DeliveryJob): Promise { const [delivery, endpoint, event] = await Promise.all([ db.webhookDelivery.findUnique({ where: { id: job.deliveryId } }), db.webhookEndpoint.findUnique({ where: { id: job.endpointId } }), db.webhookEvent.findUnique({ where: { id: job.eventId } }) ]); if (!delivery || !endpoint || !event) { console.error('Missing data for webhook delivery', job); return; } // Prepare payload const payload = JSON.stringify({ id: event.id, type: event.type, timestamp: event.createdAt.toISOString(), data: event.data }); const timestamp = Math.floor(Date.now() / 1000); const signature = signWebhookPayload(payload, endpoint.secret, timestamp); // Attempt delivery const startTime = Date.now(); let response: Response; let error: Error | null = null; try { response = await fetch(endpoint.url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Webhook-Signature': signature, 'X-Webhook-Timestamp': timestamp.toString(), 'X-Webhook-Event-Type': event.type, 'X-Webhook-Event-ID': event.id, 'User-Agent': 'MyApp-Webhooks/1.0' }, body: payload, signal: AbortSignal.timeout(30000) // 30 second timeout }); } catch (e) { error = e as Error; } const duration = Date.now() - startTime; const attempts = delivery.attempts + 1; const success = response?.ok ?? false; // Update delivery record await db.webhookDelivery.update({ where: { id: delivery.id }, data: { status: success ? 'success' : (attempts >= 5 ? 'failed' : 'pending'), attempts, lastAttemptAt: new Date(), nextAttemptAt: success ? null : getNextAttemptTime(attempts), response: { statusCode: response?.status ?? 0, body: error?.message ?? (await response?.text().catch(() => '')), duration } } }); // Schedule retry if needed if (!success && attempts < 5) { const delay = RETRY_DELAYS[attempts] * 1000; setTimeout(() => { redis.lpush('webhook:deliveries', JSON.stringify(job)); }, delay); } // Log for monitoring console.log('Webhook delivery', { deliveryId: delivery.id, success, attempts, duration, statusCode: response?.status }); } function getNextAttemptTime(attempts: number): Date { const delay = RETRY_DELAYS[Math.min(attempts, RETRY_DELAYS.length - 1)]; return new Date(Date.now() + delay * 1000); } ``` --- ## Webhook Management API ```typescript // app/api/webhooks/route.ts import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { db } from '@/lib/db'; import { generateWebhookSecret } from '@/lib/webhooks/signature'; import { WEBHOOK_EVENTS } from '@/lib/webhooks/types'; import { z } from 'zod'; const createWebhookSchema = z.object({ url: z.string().url(), events: z.array(z.enum(Object.keys(WEBHOOK_EVENTS) as [string, ...string[]])).min(1) }); export async function GET(request: Request) { const session = await getServerSession(); if (!session?.user?.tenantId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const endpoints = await db.webhookEndpoint.findMany({ where: { tenantId: session.user.tenantId }, select: { id: true, url: true, events: true, active: true, createdAt: true, _count: { select: { deliveries: { where: { status: 'failed' } } } } } }); return NextResponse.json({ data: endpoints }); } export async function POST(request: Request) { const session = await getServerSession(); if (!session?.user?.tenantId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await request.json(); const parsed = createWebhookSchema.safeParse(body); if (!parsed.success) { return NextResponse.json( { error: 'Validation failed', details: parsed.error.errors }, { status: 422 } ); } const secret = generateWebhookSecret(); const endpoint = await db.webhookEndpoint.create({ data: { tenantId: session.user.tenantId, url: parsed.data.url, events: parsed.data.events, secret, active: true } }); // Return secret only on creation return NextResponse.json({ data: { id: endpoint.id, url: endpoint.url, events: endpoint.events, secret // Show once! } }, { status: 201 }); } // app/api/webhooks/[id]/route.ts export async function DELETE( request: Request, { params }: { params: { id: string } } ) { const session = await getServerSession(); if (!session?.user?.tenantId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } await db.webhookEndpoint.deleteMany({ where: { id: params.id, tenantId: session.user.tenantId } }); return new NextResponse(null, { status: 204 }); } // app/api/webhooks/[id]/test/route.ts export async function POST( request: Request, { params }: { params: { id: string } } ) { const session = await getServerSession(); if (!session?.user?.tenantId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const endpoint = await db.webhookEndpoint.findFirst({ where: { id: params.id, tenantId: session.user.tenantId } }); if (!endpoint) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); } // Send test event const testEvent = { id: 'evt_test', type: 'webhook.test', timestamp: new Date().toISOString(), data: { message: 'This is a test webhook' } }; const payload = JSON.stringify(testEvent); const timestamp = Math.floor(Date.now() / 1000); const signature = signWebhookPayload(payload, endpoint.secret, timestamp); try { const response = await fetch(endpoint.url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Webhook-Signature': signature, 'X-Webhook-Timestamp': timestamp.toString() }, body: payload, signal: AbortSignal.timeout(10000) }); return NextResponse.json({ success: response.ok, statusCode: response.status }); } catch (error) { return NextResponse.json({ success: false, error: (error as Error).message }); } } ``` --- ## Receiving Webhooks ```typescript // Example: Receiving webhooks in your app // app/api/webhooks/external/route.ts import { NextResponse } from 'next/server'; import { verifyWebhookSignature } from '@/lib/webhooks/signature'; export async function POST(request: Request) { const body = await request.text(); const signature = request.headers.get('X-Webhook-Signature'); const timestamp = parseInt(request.headers.get('X-Webhook-Timestamp') || '0'); if (!signature || !timestamp) { return NextResponse.json({ error: 'Missing signature' }, { status: 400 }); } const secret = process.env.WEBHOOK_SECRET!; try { const isValid = verifyWebhookSignature(body, signature, secret, timestamp); if (!isValid) { return NextResponse.json({ error: 'Invalid signature' }, { status: 401 }); } } catch (error) { return NextResponse.json({ error: (error as Error).message }, { status: 401 }); } const event = JSON.parse(body); // Process event switch (event.type) { case 'project.created': await handleProjectCreated(event.data); break; case 'user.created': await handleUserCreated(event.data); break; default: console.log('Unhandled event type:', event.type); } return NextResponse.json({ received: true }); } ``` --- ## Best Practices | Aspect | Recommendation | |--------|----------------| | **Signing** | HMAC-SHA256 with timestamp | | **Timeouts** | 30 seconds max | | **Retries** | Exponential backoff, max 5 | | **Idempotency** | Include event ID for dedup | | **Logging** | Log all attempts | | **Testing** | Provide test endpoint | --- ## Fazit Webhooks erfordern: 1. **Security**: Signierung und Verifikation 2. **Reliability**: Retry-Logic mit Backoff 3. **Observability**: Logging und Monitoring 4. **Developer UX**: Einfache Konfiguration Webhooks sind die Basis für Event-Driven Integrations. --- ## Bildprompts 1. "Webhook delivery flow diagram, event to endpoint" 2. "Webhook management dashboard, endpoints and delivery status" 3. "Retry strategy visualization, exponential backoff" --- ## Quellen - [Stripe Webhooks Best Practices](https://stripe.com/docs/webhooks/best-practices) - [GitHub Webhooks](https://docs.github.com/en/webhooks) - [Webhook.site for Testing](https://webhook.site/) - [Standard Webhooks Specification](https://www.standardwebhooks.com/)