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. We'll do it in around 5 minutes with stripe.
Besides the obvious benefit of making money from your apps, taking payment early:
- allows people to tell you if you if you're building something they want
- fuels your willpower.
We'll:
- 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
)—that’s 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—done.
Mental model (60 sec)
Think of Stripe Checkout as a hosted cash register.
Your app:
- Hands Stripe a price ID (create session).
- Sends the shopper to that register (redirect).
- Waits to be paged when the drawer closes (webhook).
That’s it!
Common mistakes
| Issue | Quick fix |
| ---------------------------------------------- | ---------------------------------------------------------------------- |
| StripeSignatureVerificationError
| Check STRIPE_WEBHOOK_SECRET
matches the CLI output |
| undefined NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
| Env var missing in deployment |
| 404 on /api/checkout
| File path must be /app/api/checkout/route.ts
(note the route.ts) |
You now have Stripe payments live with ~60 lines of code.
Start charging, iterate later.