Stripe

Stripe Checkout with Next.js

Create a Stripe Checkout Session from a Next.js route handler, for subscriptions and for one-time payments.

2 min read

Stripe Checkout is a hosted page. Your Next.js app creates a Session and redirects to session.url. You never collect the card number. The Stripe Subscription Starter uses this for a recurring Pro price. The Job Board Starter and the Marketplace Starter use the same API with mode: "payment".

Subscription session

This is the call in src/app/api/stripe/checkout/route.ts of the subscription starter. The Stripe client is constructed inside the request so a missing STRIPE_SECRET_KEY throws a clear error instead of failing at import time during next build.

import Stripe from "stripe";

export function stripe() {
  const key = process.env.STRIPE_SECRET_KEY;
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set");
  return new Stripe(key);
}
const session = await stripe().checkout.sessions.create({
  mode: "subscription",
  customer,
  line_items: [{ price: process.env.STRIPE_PRO_PRICE_ID || "", quantity: 1 }],
  success_url: `${process.env.APP_URL}/dashboard`,
  cancel_url: `${process.env.APP_URL}/pricing`,
  metadata: { userId: user.id }
});

customer is a Stripe customer id (cus_...), not your database id. Create it first, save it on the user, and pass it on the next visit so Checkout does not mint a second customer for the same email.

One-time session

A job posting or a marketplace listing is not a subscription. The marketplace kit sends mode: "payment" and an inline price in cents:

await stripe().checkout.sessions.create({
  mode: "payment",
  line_items: [{
    quantity: 1,
    price_data: {
      currency: "eur",
      unit_amount: listing.priceCents,
      product_data: { name: listing.title }
    }
  }],
  success_url: `${process.env.APP_URL}/dashboard?paid=1`,
  cancel_url: `${process.env.APP_URL}/listings/${listing.slug}`,
  metadata: { listingId: listing.id }
});

unit_amount is an integer in the currency’s minor unit. A listing stored as priceCents: 2400 is 24.00 EUR. Do not pass a float.

The job board uses a Dashboard price id (STRIPE_JOB_PRICE_ID) instead of price_data, and puts {CHECKOUT_SESSION_ID} in the success URL so the return route can retrieve the session. That pattern is in accepting paid job submissions.

Redirect status

NextResponse.redirect(session.url) is correct for a GET handler. The marketplace route is a POST and redirects with status 303 so the browser follows with GET. If session.url is missing, send the user back to pricing rather than redirecting to "null".

Checkout collects the payment. It does not update your database by itself. Pair every session with either a webhook or a success handler that reads payment_status from Stripe. See Stripe webhooks with Next.js.

Want to skip the setup? The Stripe Subscription Starter already includes a Checkout route for a subscription price, plus the webhook that records it.

Product updates for developers shipping with Designyff kits.

Purchased source may be used in personal, commercial, and client projects. Do not redistribute or resell the original source. Terms of Service

Designyff © 2026 | Starter kits for developers | Powered by  Tacko