How to add Stripe to Next.js in 5 minutes 💰
What we'll do
We'll build a simple Next.js app that allows users to buy a product in around 5 minutes with Stripe.
What we'll do:
- use Stripe Checkout to handle the payment.
- use the Stripe Dashboard to create a product and price.
- use the Stripe API to create a checkout session.
Let's go 🚀 I'll use pnpm
for the commands. Feel free to use your favorite package manager.
1. Install Stripe
pnpm add stripe @stripe/stripe-js
2. Environment variables
Create .env
(or .env.local
):
# Stripe
STRIPE_SECRET_KEY=sk_test_…
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_…
STRIPE_WEBHOOK_SECRET=whsec_… # filled after step 6
# App base URL (used in redirect links)
NEXT_PUBLIC_APP_URL=http://localhost:3000
3. Add your Checkout session API route
- One endpoint, one Stripe call. No cookies, no auth required.
src/app/api/checkout/route.ts
import Stripe from 'stripe'
import { NextResponse } from 'next/server'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
})
export async function POST(req: Request) {
const { priceId } = await req.json()
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/cancel`,
})
return NextResponse.json({ url: session.url })
}
4. Add a client helper
getStripe.ts
(already in the starter):
import { loadStripe } from '@stripe/stripe-js'
let stripePromise: ReturnType<typeof loadStripe>
export const getStripe = () => {
if (!stripePromise)
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)
return stripePromise
}
5. Redirect button
'use client'
import { useState } from 'react'
import { getStripe } from '@/getStripe'
export default function PayButton({ priceId }: { priceId: string }) {
const [loading, setLoading] = useState(false)
const handleClick = async () => {
setLoading(true)
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ priceId }),
})
const { url, error } = await res.json();
if (error) return alert(error);
window.location.href = url;
}
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Redirecting…' : 'Buy'}
</button>
)
}
6. Add a webhook endpoint
This is necessary for Stripe to tell you when a payment is successful. This lets you mark the order as paid in your database and unlock any functionality for the user.
src/app/api/webhook/route.ts
import { type NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY is not set");
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function POST(req: NextRequest) {
const signature = req.headers.get("stripe-signature");
if (!signature) {
return new NextResponse("Missing stripe-signature header", { status: 400 });
}
const body = Buffer.from(await req.arrayBuffer());
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET as string,
);
} catch {
return new NextResponse("Invalid signature", { status: 400 });
}
if (event.type === "checkout.session.completed") {
console.log("Great! Cash for us. Event: checkout.session.completed");
// You would update your database here to show the user has paid.
}
return NextResponse.json({ received: true });
}
To test this webhook locally
Start a listener in another terminal:
stripe listen --forward-to localhost:3000/api/webhook
The command prints whsec_…
—paste that into STRIPE_WEBHOOK_SECRET
.
Webhooks let you trust Stripe, not the browser, for payment state.
7. Create a product & price
- In your Stripe Dashboard → Products → + Add product.
- Copy the Price ID (
price_123
). This is what you pass to the API route.
Nothing to migrate or seed; Stripe is your product catalog.
8. Run locally
pnpm dev # http://localhost:3000
Click Buy → pay with the details from the Stripe test card details
see the success page.
9. Deploy
The repo is Vercel-ready:
vercel --prod
Add the three Stripe env vars in the Vercel dashboard.
We're done.
Common mistakes
StripeSignatureVerificationError
: Check yourSTRIPE_WEBHOOK_SECRET
matches the CLI output.undefined NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
: Check if the env var is missing in deployment.- 404 on
/api/checkout
: Check if the file path is/app/api/checkout/route.ts
(note the route.ts).