Guide · April 2026

Stripe Webhooks in Next.js: 8 rules for zero 3am pages

A checklist from building Stripe integrations across 5 SaaS products.

Rule 1: Use nodejs runtime, not Edge

Edge runtime can't reliably read the raw request body. Stripe's constructEvent needs the exact raw string for signature verification.

// app/api/webhooks/stripe/route.ts
export const runtime = 'nodejs';  // NOT 'edge'
export const dynamic = 'force-dynamic';

Rule 2: Raw body before JSON

Never req.json() before signature verification. Use req.text() and pass the raw string to constructEvent.

const body = await req.text();  // raw string
const sig = headers().get('stripe-signature');
const event = stripe.webhooks.constructEvent(body, sig, secret);

Rule 3: Webhook is the source of truth

Never trust the checkout success redirect to confirm payment. The redirect can fail, the user can close the tab, the network can drop. Only mark orders as paid in the webhook handler.

Rule 4: Make handlers idempotent

Stripe retries on non-2xx for up to 72 hours. Your handler will be called multiple times. Use upsert by subscription ID or a dedupe table keyed by event ID.

Rule 5: Don't throw on expected conditions

If the event is "already processed" or metadata is missing, log and return 200. Throwing triggers infinite retries.

Rule 6: Store money as integer cents

Never float. Never numeric. Use bigint or integer in Postgres, number in TypeScript. $39.00 = 3900.

Rule 7: Never log full event payloads

Stripe events contain PII — email, last4 of card, billing addresses. Log event type and ID only.

Rule 8: Test with Stripe CLI

stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed

Automate all 8 rules

The @agent-stripe-setup subagent enforces every rule in this guide automatically. Part of the Claude Code Subagents Pack.

Get the Pack — $39