Next.js cookbook · Intermediate

Receive Stripe webhooks in a Next.js Route Handler

Process payment events securely in a Next.js app deployed to serverless.

Approach

In a Route Handler, read the raw body with await request.text(), verify with the provider SDK (stripe.webhooks.constructEvent) using the signing secret, deduplicate by event ID, do minimal work and return 200. Run on the Node.js runtime when the SDK needs Node APIs.

Solution

// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { stripe } from "@/lib/stripe";

export const runtime = "nodejs";

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("stripe-signature");
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, signature!, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }

  const inserted = await db.webhookEvent.createMany({ data: [{ id: event.id, type: event.type }], skipDuplicates: true });
  if (inserted.count === 0) return new Response("Duplicate", { status: 200 });

  switch (event.type) {
    case "checkout.session.completed":
      await orders.markPaid((event.data.object as Stripe.Checkout.Session).metadata!.orderId);
      break;
    case "customer.subscription.deleted":
      await subscriptions.cancel((event.data.object as Stripe.Subscription).id);
      break;
  }
  return Response.json({ received: true });
}

Pitfalls

  • Parsing JSON before verification (request.json() loses the raw body).
  • Long processing inside the handler (timeouts → retries).
  • Assuming events arrive in order.

Variations

  • Queue heavy work (Inngest, QStash, a worker) and return immediately.

Further reading: Stripe: Webhooks quickstart

Reference: https://docs.stripe.com/webhooks/quickstart?lang=node

More Next.js recipes