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
):
# StripeSTRIPE_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 } = await res.json();(await getStripe())!.redirectToCheckout({ 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 { NextRequest, NextResponse } from 'next/server'import Stripe from 'stripe'const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {apiVersion: '2023-10-16',})export async function POST(req: NextRequest) {const sig = req.headers.get('stripe-signature')!const buf = await req.arrayBuffer()const event = stripe.webhooks.constructEvent(buf,sig,process.env.STRIPE_WEBHOOK_SECRET!,)if (event.type === 'checkout.session.completed') {// TODO: mark order as 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).