Pages Router requires two things: disabling the body parser for the webhook route, and reading the raw body with micro. Miss either one and verification fails.
npm install micro
// pages/api/webhooks/stripe.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { buffer } from 'micro';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const secret = process.env.STRIPE_WEBHOOK_SECRET!;
// FIX: Disable bodyParser for this route only
export const config = {
api: { bodyParser: false },
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
return res.status(405).end('Method Not Allowed');
}
// Read raw body before any parsing
const rawBody = (await buffer(req)).toString('utf8');
const sig = req.headers['stripe-signature'] as string;
if (!sig) {
return res.status(400).json({ error: 'Missing stripe-signature header' });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(rawBody, sig, secret);
} catch (err: any) {
return res.status(400).json({ error: \`Webhook Error: \${err.message}\` });
}
switch (event.type) {
case 'checkout.session.completed':
// fulfill order
break;
case 'customer.subscription.deleted':
// cancel subscription
break;
}
res.json({ received: true });
}
bodyParser: false config export is deprecated in App Router and does nothing. Use request.text() instead. See: Stripe + Next.js App Router fix.# Test locally with Stripe CLI stripe listen --forward-to localhost:3000/api/webhooks/stripe stripe trigger checkout.session.completed
Generate the complete Pages Router handler with one click.
Open WebhookFix →