Loading...
Loading...
> Production-grade webhook system with 22 event types, HMAC-SHA256 signature verification, and automatic retry with exponential backoff.
The webhooks system allows your application to send HTTP callbacks to external services when specific events occur. This is essential for integrating with third-party services, triggering external workflows, and keeping systems in sync.
Register a webhook endpoint to receive events
1// API Route: POST /api/v1/webhooks2import { auth } from "@/lib/auth";3import { prisma } from "@/lib/db";4import { generateWebhookSecret } from "@/lib/webhooks/server";56export async function POST(req: Request) {7 const session = await auth();8 if (!session?.user) {9 return Response.json({ error: "Unauthorized" }, { status: 401 });10 }1112 const { url, events, description } = await req.json();1314 // Generate a unique secret for this webhook15 const secret = generateWebhookSecret();1617 const webhook = await prisma.webhook.create({18 data: {19 url,20 events,21 description,22 secret,23 userId: session.user.id,24 isActive: true,25 },26 });2728 return Response.json({29 id: webhook.id,30 url: webhook.url,31 events: webhook.events,32 secret: webhook.secret, // Show once on creation33 });34}
Trigger webhook deliveries from your application
1// src/lib/webhooks/server.ts2import crypto from "crypto";3import { prisma } from "@/lib/db";45export async function sendWebhook(6 event: string,7 payload: Record<string, any>,8 userId?: string9) {10 // Find all active webhooks subscribed to this event11 const webhooks = await prisma.webhook.findMany({12 where: {13 isActive: true,14 events: { has: event },15 ...(userId && { userId }),16 },17 });1819 for (const webhook of webhooks) {20 await deliverWebhook(webhook, event, payload);21 }22}2324async function deliverWebhook(webhook, event, payload) {25 const body = JSON.stringify({26 event,27 payload,28 timestamp: new Date().toISOString(),29 });3031 // Generate HMAC signature32 const signature = crypto33 .createHmac("sha256", webhook.secret)34 .update(body)35 .digest("hex");3637 const response = await fetch(webhook.url, {38 method: "POST",39 headers: {40 "Content-Type": "application/json",41 "X-Webhook-Signature": signature,42 "X-Webhook-Event": event,43 },44 body,45 });4647 // Log delivery48 await prisma.webhookDelivery.create({49 data: {50 webhookId: webhook.id,51 event,52 payload,53 statusCode: response.status,54 success: response.ok,55 },56 });57}5859// Usage60await sendWebhook("user.created", {61 id: user.id,62 email: user.email,63 name: user.name,64});
When receiving webhooks, always verify the signature
1// In your webhook receiver endpoint2import crypto from "crypto";34export async function POST(req: Request) {5 const body = await req.text();6 const signature = req.headers.get("X-Webhook-Signature");7 const event = req.headers.get("X-Webhook-Event");89 // Your webhook secret (stored securely)10 const secret = process.env.WEBHOOK_SECRET!;1112 // Verify signature using timing-safe comparison13 const expectedSignature = crypto14 .createHmac("sha256", secret)15 .update(body)16 .digest("hex");1718 const isValid = crypto.timingSafeEquals(19 Buffer.from(signature || ""),20 Buffer.from(expectedSignature)21 );2223 if (!isValid) {24 return Response.json({ error: "Invalid signature" }, { status: 401 });25 }2627 // Process the webhook28 const data = JSON.parse(body);29 console.log(`Received ${event}:`, data.payload);3031 return Response.json({ received: true });32}
Failed webhooks are automatically retried with exponential backoff
1// Retry configuration2const RETRY_CONFIG = {3 maxRetries: 5,4 initialDelay: 60, // 1 minute5 maxDelay: 3600, // 1 hour6 backoffMultiplier: 2,7};89// Retry delays: 1min, 2min, 4min, 8min, 16min1011async function queueWebhookRetry(12 webhookId: string,13 event: string,14 payload: Record<string, any>,15 attempt: number = 116) {17 if (attempt > RETRY_CONFIG.maxRetries) {18 // Mark webhook as failed after max retries19 await prisma.webhookDelivery.update({20 where: { id: deliveryId },21 data: { status: "FAILED" },22 });23 return;24 }2526 const delay = Math.min(27 RETRY_CONFIG.initialDelay * Math.pow(RETRY_CONFIG.backoffMultiplier, attempt - 1),28 RETRY_CONFIG.maxDelay29 );3031 // Schedule retry (using your job queue)32 await scheduleJob("webhook:retry", {33 webhookId,34 event,35 payload,36 attempt: attempt + 1,37 }, { delay: delay * 1000 });38}
Available webhook event types organized by category: