Skip to content
Next.js+Stripe
Stack Integration

Next.js + Stripe Integration Guide

Stripe in a Next.js App Router project requires server-side handling for webhooks, checkout sessions, and the customer portal. Never expose secret keys to client components.

Use Cases
  1. Stripe Checkout for one-time and subscription payments
  2. Webhook handler in Next.js Route Handler
  3. Customer portal for billing management
  4. Stripe secret key isolated to server components and API routes
Implementation

Create a app/api/webhooks/stripe/route.ts handler. Verify the webhook signature with stripe.webhooks.constructEvent. Checkout sessions created server-side — the client only receives a redirect URL. Use environment variables for all Stripe keys, never import in client components.

In detail

How the pieces connect

Three boundaries matter in an App Router project. The browser only ever touches Stripe-hosted surfaces: it receives a Checkout Session url and is redirected to checkout.stripe.com, or it loads the Billing Portal via a returned URL. Your Route Handlers (app/api/.../route.ts) run on the server, hold the secret key, and call stripe.checkout.sessions.create and stripe.billingPortal.sessions.create. Stripe itself calls back into your app at app/api/webhooks/stripe/route.ts after the payment completes.

The redirect flow and the webhook are independent. The success_url returns the customer to your site, but it is not proof of payment — a user can close the tab. Treat the checkout.session.completed webhook as the only authoritative signal for fulfillment, and write to your database from there, not from the success page.

Webhook verification and idempotency

A webhook Route Handler must read the raw request body. In App Router that means await req.text() — not req.json() — because stripe.webhooks.constructEvent(body, sig, secret) recomputes the HMAC over exact bytes. Parsing to JSON first re-serializes and breaks the signature. Pull the signature from req.headers.get('stripe-signature') and use the endpoint's whsec_... secret, which differs per endpoint and between the Stripe CLI (stripe listen) and production.

Stripe retries on any non-2xx response and can deliver the same event more than once, so handlers must be idempotent. Persist each event.id and short-circuit if you have already processed it, or upsert keyed on a stable id (the session or subscription id). Return 200 quickly; do slow work after acknowledging, or Stripe keeps retrying.

Secret handling and runtime

The secret key (sk_...) and webhook secret (whsec_...) live only in server-side environment variables and must never appear in a Client Component or a NEXT_PUBLIC_ variable — anything NEXT_PUBLIC_ is inlined into the client bundle. Only the publishable key (pk_...) is safe to expose. Instantiate the Stripe client in modules that never get imported by "use client" components.

Watch the runtime target. The Stripe Node SDK expects Node APIs, so a webhook handler should run on the Node.js runtime, not Edge — set export const runtime = 'nodejs' if your project defaults otherwise. Also disable any body parsing or caching: webhook and Checkout handlers are dynamic, so avoid static optimization on those routes.

When this combo fits

Next.js plus Stripe is a strong fit when you control a server runtime (Vercel, a Node host, or containers) and want checkout, subscriptions, and a self-serve billing portal without building a payments backend. Route Handlers give you exactly the server surface Stripe needs for session creation and webhook verification.

It fits less cleanly for a fully static export (output: 'export'), which has no server to receive webhooks or hold the secret key — you would need a separate function for that. It is also overkill for a single fixed-price link, where a no-code Stripe Payment Link beats writing handlers. The moment you need usage tied to accounts, plan changes, or proration, the SDK-plus-webhook path earns its keep.

Other integration guidesView all →
Related

Need this built?