Requex.me LogoRequex.me

Documentation

Browse by section

Keep all guides, tool docs, automation recipes, and comparison pages in one navigable place.

Docs Home
Docs

Foundation docs for getting started fast, understanding key terms, and tracking what has changed.

Guides

Start with fundamentals, then move into provider-specific webhook testing and production hardening.

Tool Docs

These pages explain what each tool does, when to use it, and how it fits into a webhook debugging workflow.

Automation Docs

Use these setup guides when you want forwarding rules, custom responses, security checks, or multi-destination fanout.

Compare

Use these pages to compare developer workflows, pricing tradeoffs, and feature differences between webhook tools.

Webhook Handler in Next.js

Receive webhook POST requests in Next.js App Router, verify HMAC signatures, and respond in milliseconds — complete TypeScript examples.

Editorially reviewed by the Requex team12 min readAbout the product

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 →