1 min read
BackendWebhook Implementation
Webhooks implementieren für Event-Driven Architecture. Signierung, Retry-Logic und Webhook Management für SaaS.
WebhooksEvent-DrivenSignature VerificationRetry LogicWebhook DeliveryIntegration

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
// lib/webhooks/types.ts
export interface WebhookEndpoint {
id: string;
tenantId: string;
url: string;
secret: string;
events: string[];
active: boolean;
createdAt: Date;
metadata?: Record<string, string>;
}
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
// 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
// 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<string> {
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
// 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<void> {
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<void> {
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
// 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
// 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:
- Security: Signierung und Verifikation
- Reliability: Retry-Logic mit Backoff
- Observability: Logging und Monitoring
- Developer UX: Einfache Konfiguration
Webhooks sind die Basis für Event-Driven Integrations.
Bildprompts
- "Webhook delivery flow diagram, event to endpoint"
- "Webhook management dashboard, endpoints and delivery status"
- "Retry strategy visualization, exponential backoff"