Webhook Handler in Next.js
Receive webhook POST requests in Next.js App Router, verify HMAC signatures, and respond in milliseconds — complete TypeScript examples.
TL;DR: Create a route handler at app/api/webhook/route.ts. Use await request.arrayBuffer() to get the raw bytes for signature verification — do not call request.json() first or you will corrupt the body.
Basic App Router Webhook Handler
In the App Router, API routes are Route Handlers — files named route.ts inside an app/api/ directory. Export an async function named after the HTTP method you want to handle:
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
const eventType = request.headers.get('x-event-type');
console.log('Webhook received:', eventType, body);
return NextResponse.json({ received: true });
}This is the simplest possible handler. It works for any JSON webhook that doesn't require signature verification. Always return a response quickly — Next.js serverless functions (and Vercel edge functions) have execution time limits.
Raw Body for HMAC Verification
The App Router's NextRequest is based on the Web Request API. The body stream can only be consumed once — if you call request.json(), you cannot read the raw bytes afterwards. For HMAC verification, always read the body as arrayBuffer() first:
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createHmac } from 'crypto';
export async function POST(request: NextRequest) {
const rawBody = await request.arrayBuffer();
const bodyBuffer = Buffer.from(rawBody);
const signature = request.headers.get('x-signature-256') ?? '';
const secret = process.env.WEBHOOK_SECRET!;
const expected = createHmac('sha256', secret)
.update(bodyBuffer)
.digest('hex');
if (signature !== `sha256=${expected}`) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const payload = JSON.parse(bodyBuffer.toString());
// Handle payload...
return NextResponse.json({ received: true });
}Stripe Webhook in Next.js
Stripe uses its own SDK to verify webhook signatures. It needs the raw body buffer, the Stripe-Signature header, and your webhook signing secret (from the Stripe Dashboard). Install the Stripe SDK with npm install stripe:
// app/api/stripe-webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: NextRequest) {
const rawBody = await request.arrayBuffer();
const bodyBuffer = Buffer.from(rawBody);
const sig = request.headers.get('stripe-signature') ?? '';
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
bodyBuffer,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
switch (event.type) {
case 'payment_intent.succeeded':
// Handle successful payment
break;
case 'customer.subscription.deleted':
// Handle cancellation
break;
}
return NextResponse.json({ received: true });
}Use Requex to capture the Stripe test payload first so you can verify the exact header names and signature format before wiring up your handler.
Background Processing with waitUntil
On Vercel, you can use waitUntil from the @vercel/functions package to continue processing after the response is sent, without blocking the response time:
import { waitUntil } from '@vercel/functions';
export async function POST(request: NextRequest) {
const body = await request.json();
// Fire background work — does not block the response
waitUntil(processWebhookEvent(body));
return NextResponse.json({ received: true });
}
async function processWebhookEvent(payload: unknown) {
// Safe to do slow work here
await db.insert(payload);
await sendNotification(payload);
}Without Vercel, use a fire-and-forget pattern: void processWebhookEvent(payload) — just be aware this is not retried if it fails. For reliability, push to a queue instead.
Disabling Body Parsing (Pages Router)
If you're using the older Pages Router (pages/api/), Next.js automatically parses the request body. You must disable this to read the raw bytes:
// pages/api/webhook.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export const config = {
api: { bodyParser: false },
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
}
const rawBody = Buffer.concat(chunks);
// Verify HMAC with rawBody, then parse...
res.json({ received: true });
}For all new projects, prefer the App Router pattern shown above — it is cleaner and does not require disabling any built-in parsing.
Test Your Next.js Webhook Handler
Capture the real provider payload in Requex first, then replay it against your route handler with curl.
Open Requex →Related guides
Start Testing Webhooks Now
Generate your unique URL and test webhooks instantly. Free, no signup.
Open Webhook Tester →