Skip to main content
Stripe

How to add Stripe to Next.js (App Router, 2026)

Published May 12, 20267 min read
How to add Stripe to Next.js (App Router, 2026)

How to add Stripe to Next.js (App Router, 2026)

Stripe in Next.js looks like a five-minute job until you hit the parts that only matter under real invoices: webhook idempotency, the customer portal, failed payments, proration. This is the shape of the integration that survives the first month of production.

The three modules you need

A working Stripe wiring in App Router is rarely a single file. Split it into three so each piece has one job.

1. Server-only Stripe client. Mark the module import "server-only" so a stray client component cannot import your secret key. Construct the client lazily, read process.env.STRIPE_SECRET_KEY inside a function, not at the top of the module, so a missing env var does not crash next build during Collecting page data. The lazy Proxy pattern we documented separately works here: build passes, runtime throws clearly if the key is missing.

2. Checkout Route Handler. A POST /api/checkout/route.ts that calls stripe.checkout.sessions.create({...}) and returns the session URL. Always pass client_reference_id (your internal user or workspace id), set customer_email when you have one, and use metadata for the plan id you can read back in the webhook. Pin apiVersion in your client config and hedge model claims to your current Stripe API version (verify in current Stripe docs, they ship breaking changes more often than the libraries imply).

3. Webhook Route Handler. A POST /api/webhooks/stripe/route.ts that verifies the signature with stripe.webhooks.constructEvent(rawBody, signature, secret). Read the raw body with await req.text(), not req.json(), JSON parsing strips bytes the signature was computed over. Without raw-body verification, every "test" with --forward-to works and every production webhook fails.

What survives production

The defaults Stripe shows you in the docs are not enough. Three additions you will regret skipping:

  • Idempotency keys on writes. Pass idempotencyKey: <stable-id> on stripe.subscriptions.update, stripe.customers.create, and any other write you might retry. A network hiccup on retry without one creates duplicate customers and double-charges.
  • Webhook handler runs in <2s, then offloads. Stripe expects a 2xx fast. Do the signature check, write a row to your stripe_events table with the event id as the primary key (this is your idempotency boundary), and return 200. Heavy work, sending email, updating downstream services, runs in a queue or a follow-up worker.
  • Customer portal link. Add a POST /api/billing/portal that calls stripe.billingPortal.sessions.create({ customer: customerId, return_url }). The portal handles cancellation, plan changes, invoice history, payment method updates. Without it, every cancellation becomes a support ticket.

For credits and usage-based billing on top of subscriptions, the metering layer is where most templates stop short. /features/stripe-credits walks through the credit ledger pattern, debit on token use, top up via webhook on invoice.paid, expose the balance on the dashboard.

When to skip raw Stripe

Raw Stripe gives you control. It also gives you the EU VAT, sales tax, chargeback flow, and 1099-K reporting surface. For digital goods sold globally to consumers and small businesses, a merchant of record (Paddle, Polar, Lemon Squeezy) handles all of that for a percentage point on top of fees. If you are a one-person team selling worldwide, the MoR route trades margin for hours of compliance work, usually the right trade.

If you want this wired end-to-end, Stripe client, checkout, webhooks, customer portal, credits, and a working dashboard, both /saasforge-core (subscriptions + portal + webhooks + audit) and /saasforge-ai (subscriptions + credit metering) ship it as production code. Start there if you would rather edit a working integration than build one from a Stripe docs page.

Newsletter

Get the BoilerlyKit newsletter

Practical Next.js SaaS launch tips, delivered when we ship something worth sharing.

We respect your inbox. See our privacy policy.