Next.js (App Router)TypeScript / JavaScript
Next.js App Router Webhook Guide: Raw Body & HMAC Signature Verification
Learn how to handle incoming webhooks (Stripe, Clerk, Resend, Shopify) in Next.js 14 & 15 App Router. Correctly read raw bodies and verify signatures.
Direct Answer / Quick Implementation Guide
In Next.js App Router (`app/api/webhook/route.ts`), use `await req.text()` to extract the raw unmodified payload string required for HMAC signature verification before parsing it with `JSON.parse()`. Do not use `req.json()` directly, as JSON normalization breaks signature hashing.
Essential Implementation Takeaways
- •Use `await req.text()` to obtain the raw string for cryptographic signature checks.
- •Always return `new Response(null, { status: 200 })` quickly to avoid timeouts.
- •Store signing secrets in `.env.local` as `WEBHOOK_SECRET`.
- •Use SafeWebhook Replay Drawer to test locally at `http://localhost:3000/api/webhook`.
Common Gotchas in Next.js (App Router) & How to Fix Them
Calling `await req.json()` before signature verification
Fix:Call `const body = await req.text()` first, verify the signature with `body`, and then call `JSON.parse(body)`.
Missing Webhook Secret environment variable
Fix:Add `STRIPE_WEBHOOK_SECRET=whsec_...` to `.env.local` and restart your Next.js dev server.
Complete Production Boilerplate
TypeScript / JavaScript// app/api/webhook/route.ts (Next.js 14/15 App Router)
import { headers } from 'next/headers';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-06-20',
});
export async function POST(req: Request) {
const body = await req.text(); // Critical: Read raw text
const headerList = await headers();
const signature = headerList.get('stripe-signature');
if (!signature) {
return new Response('Missing signature', { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err: any) {
console.error(`Webhook signature verification failed: ${err.message}`);
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
// Handle specific event types
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object as Stripe.PaymentIntent;
console.log('Payment succeeded:', paymentIntent.id);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return new Response(JSON.stringify({ received: true }), { status: 200 });
}Step-by-Step Setup Guide
1
Create `app/api/webhook/route.ts` in your Next.js project.
2
Import your signature verification library (e.g. Stripe, Svix, TweetNaCl).
3
Read the raw body via `const body = await req.text()`.
4
Extract signature headers with `const headerList = await headers()`.
5
Verify the signature inside a `try...catch` block and return 200 OK.
How to Test Locally on Your Machine
- Start your Next.js server: `npm run dev` (running on http://localhost:3000).
- Create a SafeWebhook URL at safewebhook.com/app.
- Configure your provider (Stripe, GitHub, Shopify) with the SafeWebhook URL.
- Use the SafeWebhook Replay Drawer to send payloads directly to `http://localhost:3000/api/webhook`.
Frequently Asked Questions: Next.js (App Router) Webhooks
Why can I not use `req.json()` in Next.js webhooks?
`req.json()` automatically parses the payload into a JavaScript object. When serialized back, differences in whitespace or property order will cause HMAC verification to fail.
Test Your Next.js (App Router) Webhook Handler Live
Capture payloads, simulate errors, and replay directly to your localhost server.