How to Handle Webhooks in Next.js
Webhook Endpoints in Next.js App Router
Next.js App Router (13.4+) uses file-based routing with route.ts files for API endpoints. To create a webhook handler, add a file at app/api/webhooks/route.ts:
import { NextRequest, NextResponse } from 'next/server';export async function POST(request: NextRequest) { const body = await request.json();
console.log('Webhook received:', body.type);
// Process the webhook event switch (body.type) { case 'payment.completed': await handlePayment(body.data); break; case 'user.created': await handleUserCreated(body.data); break; default: console.log('Unhandled event type:', body.type); }
return NextResponse.json({ received: true }); } ```
The App Router's POST export handles only POST requests automatically. You do not need to check the HTTP method. The NextRequest object provides access to headers, body, and query parameters.
Raw Body Access for Signature Verification
Webhook signature verification requires the raw (unparsed) request body. In the App Router, use request.text() instead of request.json() to get the raw string:
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';export async function POST(request: NextRequest) { const rawBody = await request.text(); const signature = request.headers.get('x-webhook-signature') ?? ''; const secret = process.env.WEBHOOK_SECRET!;
// Verify HMAC signature const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex');
if (!crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) )) { return NextResponse.json( { error: 'Invalid signature' }, { status: 401 } ); }
// Parse the verified body const body = JSON.parse(rawBody); await processWebhook(body);
return NextResponse.json({ received: true }); } ```
This pattern is essential for Stripe, GitHub, Shopify, and any provider that signs webhooks. Parse the body only after verification succeeds.
Disabling Body Parsing (Pages Router)
If you are using the Pages Router (pages/api/), Next.js automatically parses the request body. You need to disable this for signature verification:
// pages/api/webhooks/stripe.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { buffer } from 'micro';
import Stripe from 'stripe';export const config = { api: { bodyParser: false, }, };
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== 'POST') { return res.status(405).send('Method Not Allowed'); }
const buf = await buffer(req); const sig = req.headers['stripe-signature']!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
buf,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err: any) {
return res.status(400).send(Webhook Error: ${err.message});
}
// Process the event switch (event.type) { case 'checkout.session.completed': await fulfillOrder(event.data.object); break; }
res.status(200).json({ received: true }); } ```
The config.api.bodyParser = false setting and the buffer() function from the micro package give you access to the raw body.
Testing with ReqPour
During development, point your webhook provider at a ReqPour endpoint and use the relay to forward to your Next.js dev server:
npx reqpour relay --to http://localhost:3000/api/webhooksNext.js's development server runs on port 3000 by default. The relay preserves the full path, so a request to https://abc123.reqpour.com/api/webhooks/stripe forwards to http://localhost:3000/api/webhooks/stripe, matching your file-based route exactly.
Use the ReqPour dashboard to inspect incoming payloads before they reach your handler. This helps you understand the exact structure of each event type and build your handler with confidence.
For iterative development, use ReqPour's replay feature: trigger a real webhook event once, then replay it from the dashboard while you refine your handler code. No need to create test transactions or events in the provider repeatedly.
Middleware and Error Handling
In the App Router, you can use Next.js middleware (middleware.ts) to add cross-cutting concerns to your webhook endpoints. However, be careful — middleware that reads the body or adds authentication may interfere with webhook processing.
Exclude your webhook routes from CSRF protection, authentication middleware, and rate limiting that might block legitimate webhook deliveries. Webhook endpoints should be open to receive POST requests from the provider.
For error handling, catch exceptions in your webhook handler and return appropriate status codes. Return 200 for successful processing, 400 for invalid payloads, 401 for signature verification failures, and 500 for unexpected server errors. Returning 500 allows the provider to retry, while 400 tells the provider not to retry (since the request is permanently invalid).
export async function POST(request: NextRequest) {
try {
const rawBody = await request.text();
const event = verifyAndParse(rawBody, request.headers);
await processEvent(event);
return NextResponse.json({ received: true });
} catch (error) {
if (error instanceof SignatureError) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
console.error('Webhook processing error:', error);
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}Related
Get started with ReqPour
Catch, inspect, and relay webhooks to localhost. Free to start, $3/mo for Pro.