WritingHow to Integrate Stripe Checkout in a Next.js App (Step-by-Step Guide) — LinkIT
4 min readstripe, nextjs, payments, stripe-checkout, integration

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.

Stripe Checkout provides a hosted payment page. This example connects session creation, the client redirect, and webhook handling in Next.js. Treat it as a starting point: authentication, fulfillment rules, idempotency, and payment-method behavior need validation for your product.

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

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.

For help reviewing a Checkout integration and its billing events, contact LinkIT.