Free LessonLesson 01 of 08

Add Stripe Checkout
in 15 Minutes

Go from zero to accepting real payments in your Next.js app. We'll use Stripe Checkout — Stripe's hosted payment page — because it's the fastest, safest way to start collecting money.

~15 min readNext.js + Node.jsBeginner friendly

Why Stripe Checkout (and not a custom form)?

You could build a custom payment form with Stripe Elements. But here's why Checkout wins for your first integration:

Zero PCI hassle

Stripe hosts the payment page. Card numbers never touch your server.

Built-in UX

Apple Pay, Google Pay, address validation, error handling — all free.

Ships in minutes

~30 lines of code total. No frontend form state to manage.

Once you outgrow Checkout, you can switch to Elements. But for shipping fast? Checkout is the move. Let's build it.

Before you start

  • A Stripe account (free) — sign up at dashboard.stripe.com
  • Your test mode API keys Developers → API keys in the dashboard
  • A Next.js app (App Router) — npx create-next-app@latest
  • Node.js 18+ installed
1

Install the Stripe SDK

One package. Server-side only — no client-side Stripe.js needed for Checkout.

terminalbash
npm install stripe

Then add your secret key to .env.local:

.env.localenv
STRIPE_SECRET_KEY=sk_test_your_key_here
NEXT_PUBLIC_BASE_URL=http://localhost:3000

Never commit sk_test_ or sk_live_ keys to git. Add .env.local to your .gitignore (Next.js does this by default).

2

Create the Checkout API route

This is where the magic happens. When a user clicks “Buy,” your frontend hits this route. It creates a Stripe Checkout Session and returns the URL to redirect the user to.

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

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

export async function POST() {
  const session = await stripe.checkout.sessions.create({
    mode: "payment",
    line_items: [
      {
        price_data: {
          currency: "usd",
          product_data: {
            name: "Your Product",
          },
          unit_amount: 2999, // $29.99 in cents
        },
        quantity: 1,
      },
    ],
    success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success`,
    cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/`,
  });

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

That's it — about 20 lines. Let's break down the key parts:

mode: "payment"One-time charge. Use "subscription" for recurring billing.
price_dataInline pricing. You can also create a Price in the Stripe dashboard and pass price: "price_xxx" instead.
unit_amount: 2999Always in cents. $29.99 = 2999. This trips up everyone at least once.
success_url / cancel_urlWhere Stripe sends the customer after payment or if they bail.

unit_amount is in cents, not dollars. Writing 2999 when you mean $29.99 is correct. Writing 29.99 will charge $0.30. Ask me how I know.

3

Add the “Buy” button

A client component that calls your API route and redirects to Stripe's hosted checkout page.

app/components/CheckoutButton.tsxtsx
"use client";

export default function CheckoutButton() {
  const handleCheckout = async () => {
    const res = await fetch("/api/checkout", {
      method: "POST",
    });
    const { url } = await res.json();
    window.location.href = url;
  };

  return (
    <button
      onClick={handleCheckout}
      className="px-6 py-3 bg-indigo-600 text-white
        font-semibold rounded-lg hover:bg-indigo-500
        transition-colors"
    >
      Buy Now — $29.99
    </button>
  );
}

Drop this component anywhere in your app. When clicked, it:

  1. Calls POST /api/checkout
  2. Gets back the Stripe Checkout URL
  3. Redirects the browser to Stripe's hosted page
  4. Customer pays → redirected to your success page

Use window.location.href instead of Next.js router.push() — the Checkout URL is an external Stripe domain, not a route in your app.

4

Build the success page

After payment, Stripe redirects customers here. Keep it simple — a thank you and next steps.

app/success/page.tsxtsx
export default function Success() {
  return (
    <div className="min-h-screen flex items-center
      justify-center">
      <div className="text-center space-y-4">
        <h1 className="text-4xl font-bold">
          Payment successful! 🎉
        </h1>
        <p className="text-gray-600">
          Thanks for your purchase. Check your email
          for the receipt.
        </p>
        <a href="/" className="text-indigo-600
          hover:underline">
          ← Back to home
        </a>
      </div>
    </div>
  );
}

Pro tip: You can append ?session_id={CHECKOUT_SESSION_ID} to your success_url in step 2 to retrieve the session details and show the customer's name or email on this page. We'll cover that in a future lesson.

5

Test it

Fire up your dev server and try a test payment:

terminalbash
npm run dev

Click your Buy button, and on the Stripe Checkout page use these test card details:

Card number4242 4242 4242 4242ExpiryAny future dateCVCAny 3 digits

If everything works, you'll land on your success page. Check the Stripe Dashboard → Payments tab to see the test payment.

Common gotchas

“No such price” error

If you're using a price ID instead of price_data, make sure it matches your environment. Test mode prices start with price_ and only work with test keys.

Redirect doesn't work locally

Your NEXT_PUBLIC_BASE_URL must match your dev URL exactly: http://localhost:3000 (no trailing slash). If you're using a different port, update it.

CORS or “Failed to fetch”

This usually means your API route has a syntax error and is returning HTML (the Next.js error page) instead of JSON. Check your terminal for the actual error.

Payments work in test but fail in production

You need to swap sk_test_ for sk_live_ and make sure your Stripe account has completed onboarding (bank account connected, business details filled in).

Ship-it checklist

Before you push to production, make sure you've hit every item:

What you just built

In about 15 minutes and ~30 lines of code, you added:

  • A server-side API route that creates Stripe Checkout sessions
  • A “Buy Now” button that redirects to Stripe's hosted payment page
  • A success page for post-payment confirmation
  • A production-ready payment flow with zero PCI compliance burden

Next up: Handle Stripe Webhooks Like a Pro

Learn to verify signatures, handle checkout and invoice events, and test with the Stripe CLI. Same speed — 15 minutes.

Unlock all lessons — $14.50/mo

Cancel anytime · Instant access to all 8 lessons