How to add Stripe to Next.js in 5 minutes 💰

June 13, 2025

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

  1. In your Stripe Dashboard → Products → + Add product.
  2. 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 your STRIPE_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).

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

No spam guaranteed Unsubscribe whenever