# How to Integrate Stripe Checkout in a Next.js App (Step-by-Step Guide)

> A practical step-by-step guide to integrating Stripe Checkout into a Next.js app, covering session creation, webhooks, and production readiness.

- **Published:** 2026-02-01
- **Author:** Clixo
- **Reading time:** 4 min read
- **Tags:** stripe, nextjs, payments, stripe-checkout, integration
- **Canonical URL:** https://clixo.sh/blog/stripe-checkout-nextjs-integration-guide

Most teams reach for Stripe Checkout because it is fast to ship and offloads PCI liability. But "fast" turns into a weekend of debugging when session creation, redirects, and webhook fulfillment are not wired together correctly. This guide walks through a complete, production-ready Stripe Checkout integration in Next.js — from API route to order confirmation.

## Setting Up Stripe Checkout in a Next.js Project

### 1. Install dependencies and configure keys

```
npm install stripe @stripe/stripe-js
```

Store your keys in environment variables. Never commit secret keys to source control.

- `STRIPE_SECRET_KEY` — used on the server only
- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` — safe to expose in browser bundles
- `STRIPE_WEBHOOK_SECRET` — generated when you register your webhook endpoint

### 2. Create a Checkout Session in an API route

Stripe Checkout works by creating a session on your server, then redirecting the customer to Stripe's hosted page. In Next.js App Router:

```
// app/api/checkout/route.ts
import Stripe from "stripe";
import { NextResponse } from "next/server";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const { priceId, customerId } = await req.json();

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",          // or "payment" for one-time
    customer: customerId,          // attach to an existing customer record
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url:  `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
  });

  return NextResponse.json({ url: session.url });
}
```

On the client, call this route and redirect:

```
const res  = await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ priceId }) });
const { url } = await res.json();
window.location.href = url;
```

### 3. Never fulfill on the redirect — use webhooks

```mermaid
sequenceDiagram
  participant C as Customer
  participant App as Next.js App
  participant S as Stripe API
  participant W as Webhook Handler
  C->>App: Click Subscribe
  App->>S: POST checkout sessions create
  S-->>App: session.url
  App-->>C: Redirect to Stripe Checkout
  C->>S: Enter payment details
  S-->>C: Redirect to success_url
  S->>W: checkout.session.completed event
  W->>App: Provision access in database
```

The `success_url` redirect is unreliable. A dropped network connection after payment means your success page never loads, but the customer was still charged. Always provision access through the `checkout.session.completed` webhook event.

```
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { headers } from "next/headers";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const body = await req.text();               // must be raw bytes
  const sig  = headers().get("stripe-signature")!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch {
    return new Response("Signature verification failed", { status: 400 });
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session;
    await provisionAccess(session.customer as string, session.subscription as string);
  }

  return new Response("ok", { status: 200 });
}
```

Return `200` immediately. If your fulfillment logic throws, Stripe will retry for up to 72 hours.

## Handling the Customer Portal for Subscription Management

After checkout, customers need to cancel, upgrade, or update their payment method. Stripe's Customer Portal handles all of this without any additional UI work.

```
const portalSession = await stripe.billingPortal.sessions.create({
  customer: customerId,
  return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard`,
});
// redirect to portalSession.url
```

Enable the portal in your Stripe Dashboard under **Billing > Customer portal**.

## Testing Before Going Live

Use Stripe's test card `4242 4242 4242 4242` with any future expiry and any CVC for a successful payment. Test declined cards with `4000 0000 0000 0002`. Run your webhook handler locally using the Stripe CLI:

```
stripe listen --forward-to localhost:3000/api/webhooks/stripe
```

This streams real test events to your local server, so you can verify fulfillment logic end-to-end before touching production.

## Common Pitfalls

- **Parsing the body before signature verification** — Express middleware that parses JSON will corrupt the raw bytes Stripe uses to compute the signature. In Next.js App Router, `req.text()` gives you the raw body directly.
- **Reusing a Checkout Session URL** — Sessions expire after 24 hours. Always create a fresh session per checkout attempt.
- **Missing `customer` on the session** — Attach a Stripe Customer ID so the portal and subscription lookups work without extra joins.
- **No idempotency on session creation** — Pass an `idempotencyKey` header if you retry session creation, to avoid duplicate sessions.

## What Comes Next

A working Stripe Checkout integration is not the end. You will need to handle subscription lifecycle events — `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed` — to keep your database in sync with Stripe's state. Plan the event model before you ship.

If you need a Stripe integration built correctly from day one — with webhooks, portal, subscription lifecycle, and multi-environment setup — [Start a build](https://clixo.sh/#contact) with Clixo.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
