Guides · Free check · $9 fix pack

Guides

Next.js Stripe webhook: getting the raw body (App Router and Pages Router)

Stripe: "Stripe requires the raw body of the request to perform signature verification. If you're using a framework, make sure it doesn't manipulate the raw body." (Stripe docs). In Next.js, how you get that raw body depends on the router.

App Router (app/api/.../route.ts)

Read the body with await req.text() and pass that string to constructEvent. Next.js documents request.text() for webhooks and notes that "unlike API Routes with the Pages Router, you do not need to use bodyParser to use any additional configuration" (Next.js docs). This is the shape of Stripe's own App Router example (Stripe example):

// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string)

export async function POST(req: Request) {
  const body = await req.text() // NOT req.json()
  const signature = req.headers.get('stripe-signature') as string
  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET as string)
  } catch (err) {
    return Response.json({ message: 'Webhook Error' }, { status: 400 })
  }
  // handle event.type here
  return Response.json({ received: true }, { status: 200 })
}

The mistake to avoid: calling await req.json() and then JSON.stringify-ing it back. Re-serialising changes the bytes, and "any manipulation to the raw body of the request causes the verification to fail" (source).

Pages Router (pages/api/...)

API Routes parse the body automatically. Next.js: "bodyParser is automatically enabled", and one reason to disable it is "to verify the raw body of a webhook request" (Next.js docs). Stripe's troubleshooting table says the same for the Pages Router: disable bodyParser and read a buffer (Stripe docs; Stripe example):

// pages/api/webhooks/stripe.ts
import Stripe from 'stripe'
import type { NextApiRequest, NextApiResponse } from 'next'

export const config = { api: { bodyParser: false } }

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string)

const buffer = (req: NextApiRequest) =>
  new Promise<Buffer>((resolve, reject) => {
    const chunks: Buffer[] = []
    req.on('data', (c: Buffer) => chunks.push(c))
    req.on('end', () => resolve(Buffer.concat(chunks)))
    req.on('error', reject)
  })

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') { res.setHeader('Allow', 'POST'); return res.status(405).end() }
  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(await buffer(req), req.headers['stripe-signature'] as string, process.env.STRIPE_WEBHOOK_SECRET as string)
  } catch (err) {
    return res.status(400).send('Webhook Error')
  }
  // handle event.type here
  res.json({ received: true })
}

If it still fails

Check yours in 10 seconds (free).

Paste your webhook URL. WebhookProof sends 3 harmless test requests and tells you whether Stripe can reach it and whether it rejects unsigned events. If it finds a problem, the $9 fix pack gives you the exact copy-paste fix for your stack (refundable for 14 days).

Run the free check

FAQ

How do I get the raw body for a Stripe webhook in the Next.js App Router?

Use const body = await req.text() in the route handler and pass that string to stripe.webhooks.constructEvent. No bodyParser config is needed in the App Router.

How do I get the raw body in the Next.js Pages Router?

Export config = { api: { bodyParser: false } } from the API route, read the request stream into a Buffer, and pass the Buffer to constructEvent.

Why does req.json() break Stripe signature verification?

Stripe signs the exact bytes it sent. Parsing and re-serialising the JSON changes those bytes, so the signature no longer matches.

Sources

More guides

Stripe webhook not working? Check these 7 things in order · A customer paid but didn't get access. What went wrong? · No signatures found matching the expected signature for payload · Stripe webhook failing with a 307 or 308 redirect

Last reviewed 2026-10-18. WebhookProof is an independent tool, not affiliated with Stripe, Inc.. Quotes are from the linked documentation.