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.
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
Install the Stripe SDK
One package. Server-side only — no client-side Stripe.js needed for Checkout.
npm install stripeThen add your secret key to .env.local:
STRIPE_SECRET_KEY=sk_test_your_key_here
NEXT_PUBLIC_BASE_URL=http://localhost:3000Never commit sk_test_ or sk_live_ keys to git. Add .env.local to your .gitignore (Next.js does this by default).
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.
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.
Add the “Buy” button
A client component that calls your API route and redirects to Stripe's hosted checkout page.
"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:
- Calls
POST /api/checkout - Gets back the Stripe Checkout URL
- Redirects the browser to Stripe's hosted page
- 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.
Build the success page
After payment, Stripe redirects customers here. Keep it simple — a thank you and next steps.
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.
Test it
Fire up your dev server and try a test payment:
npm run devClick your Buy button, and on the Stripe Checkout page use these test card details:
4242 4242 4242 4242ExpiryAny future dateCVCAny 3 digitsIf 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