How to add Stripe to Next.js in 5 minutes đź’°

Published: June 13, 2025

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):

# 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 } = 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:

  1. Hands Stripe a price ID (create session).
  2. Sends the shopper to that register (redirect).
  3. 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.

P.S Want to ship better features with AI?
Join my free weekly newsletter

Each week, I share bite-sized learnings and AI news that matters - so you can build better software in practice.

No spam guaranteed · Unsubscribe whenever