Webhooks enable real-time integrations with services like Stripe, Clerk, GitHub, and more. Here is how to build secure webhook endpoints.
Step 1: Basic Webhook Endpoint
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const body = await req.text();
// Process the webhook payload
const event = JSON.parse(body);
console.log("Webhook received:", event.type);
return NextResponse.json({ received: true });
}
Step 2: Stripe Webhook with Signature Verification
pnpm add stripe
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
console.error("Webhook signature verification failed:", err);
return NextResponse.json(
{ error: "Invalid signature" },
{ status: 400 }
);
}
// Handle the event
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case "invoice.payment_succeeded": {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentSucceeded(invoice);
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCancelled(subscription);
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
// Update user subscription in database
// Send welcome email
}
async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
// Record payment
// Update billing history
}
async function handleSubscriptionCancelled(sub: Stripe.Subscription) {
// Downgrade user access
// Send cancellation email
}
Step 3: Clerk Webhook
pnpm add svix
// app/api/webhooks/clerk/route.ts
import { NextRequest, NextResponse } from "next/server";
import { Webhook } from "svix";
const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!;
export async function POST(req: NextRequest) {
const body = await req.text();
const svixId = req.headers.get("svix-id")!;
const svixTimestamp = req.headers.get("svix-timestamp")!;
const svixSignature = req.headers.get("svix-signature")!;
const wh = new Webhook(webhookSecret);
let event: any;
try {
event = wh.verify(body, {
"svix-id": svixId,
"svix-timestamp": svixTimestamp,
"svix-signature": svixSignature,
});
} catch {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
switch (event.type) {
case "user.created":
await createUserInDatabase(event.data);
break;
case "user.updated":
await updateUserInDatabase(event.data);
break;
case "user.deleted":
await deleteUserFromDatabase(event.data.id);
break;
}
return NextResponse.json({ received: true });
}
Step 4: Generic Webhook Signature Verification
For services using HMAC-SHA256 signatures:
// lib/webhook.ts
import { createHmac, timingSafeEqual } from "crypto";
export function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = createHmac("sha256", secret)
.update(payload)
.digest("hex");
try {
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch {
return false;
}
}
// Usage in route handler
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get("x-webhook-signature")!;
if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET!)) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const event = JSON.parse(body);
// Process event...
return NextResponse.json({ received: true });
}
Step 5: Idempotent Processing
Webhooks can be retried. Ensure idempotent handling:
export async function POST(req: NextRequest) {
const body = await req.text();
const event = JSON.parse(body);
const eventId = event.id;
// Check if already processed
const existing = await db
.select()
.from(processedEvents)
.where(eq(processedEvents.eventId, eventId))
.limit(1);
if (existing.length > 0) {
return NextResponse.json({ received: true, duplicate: true });
}
// Process the event
await processEvent(event);
// Mark as processed
await db.insert(processedEvents).values({
eventId,
processedAt: new Date(),
});
return NextResponse.json({ received: true });
}
Step 6: Sending Webhooks from Your App
// lib/webhooks.ts
import { createHmac } from "crypto";
interface WebhookPayload {
event: string;
data: Record<string, unknown>;
timestamp: string;
}
export async function sendWebhook(
url: string,
secret: string,
payload: WebhookPayload
) {
const body = JSON.stringify(payload);
const signature = createHmac("sha256", secret).update(body).digest("hex");
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Signature": signature,
"X-Webhook-Timestamp": payload.timestamp,
},
body,
});
if (!response.ok) {
throw new Error(`Webhook delivery failed: ${response.status}`);
}
return response;
}
Step 7: Retry Logic for Outgoing Webhooks
async function sendWithRetry(
url: string,
secret: string,
payload: WebhookPayload,
maxRetries = 3
) {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await sendWebhook(url, secret, payload);
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
Step 8: Testing Webhooks Locally
Use the Stripe CLI or ngrok to test locally:
# Stripe CLI
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# ngrok
ngrok http 3000
# Use the ngrok URL as the webhook endpoint
Need Webhook Integrations?
We build web applications with robust webhook systems, third-party integrations, and real-time event processing. Contact us to get started.