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.