A complete Stripe integration: a pricing page with a Subscribe button, a Stripe Checkout session that collects payment, and a webhook handler that upgrades the user's plan in the database when Stripe confirms the payment.
Create a Stripe Checkout Session
When a user clicks Subscribe, your API creates a Stripe Checkout Session and redirects them to Stripe's hosted payment page. Stripe handles everything: card collection, 3D Secure, receipts.
import Stripe from 'stripe'
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
// Your Stripe price ID from dashboard (price_xxx)
const PRO_PRICE_ID = 'price_xxxxx'
export async function POST() {
const session = await auth()
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// Get or create Stripe customer
let stripeCustomerId = (session.user as any).stripeCustomerId
if (!stripeCustomerId) {
const customer = await stripe.customers.create({
email: session.user.email!,
metadata: { userId: session.user.id! }
})
stripeCustomerId = customer.id
await prisma.user.update({
where: { id: session.user.id },
data: { stripeCustomerId }
})
}
// Create checkout session
const checkoutSession = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: 'subscription',
line_items: [{ price: PRO_PRICE_ID, quantity: 1 }],
success_url: `${process.env.NEXTAUTH_URL}/dashboard?upgraded=true`,
cancel_url: `${process.env.NEXTAUTH_URL}/pricing`,
})
return NextResponse.json({ url: checkoutSession.url })
}Stripe Webhook Handler
Webhooks are Stripe's way of telling you what happened. When a payment succeeds, Stripe sends a POST to your webhook endpoint. You verify it's really from Stripe and update the database.
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { prisma } from '@/lib/prisma'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
switch (event.type) {
case 'checkout.session.completed': {
const checkoutSession = event.data.object as Stripe.Checkout.Session
await prisma.user.update({
where: { stripeCustomerId: checkoutSession.customer as string },
data: { plan: 'PRO', creditsLimit: 999999 }
})
break
}
case 'customer.subscription.deleted': {
const sub = event.data.object as Stripe.Subscription
await prisma.user.update({
where: { stripeCustomerId: sub.customer as string },
data: { plan: 'FREE', creditsLimit: 10 }
})
break
}
}
return NextResponse.json({ received: true })
}Test Webhooks Locally with Stripe CLI
Stripe can't reach localhost. The Stripe CLI creates a tunnel that forwards webhook events from Stripe to your local server.
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
stripe login
# Forward webhook events to local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Your webhook signing secret is: whsec_...
# Copy this to STRIPE_WEBHOOK_SECRET in .env.local
# Trigger a test event in another terminal
stripe trigger checkout.session.completed
# Watch the first terminal — you'll see the event arrivePricing Page and Subscribe Button
The pricing page shows your plans. The Subscribe button calls your checkout API and redirects to Stripe. Keep it simple — one CTA, clear value proposition.
'use client'
export default function Pricing() {
const subscribe = async () => {
const res = await fetch('/api/stripe/checkout', { method: 'POST' })
const { url } = await res.json()
window.location.href = url // redirect to Stripe Checkout
}
return (
<div className="pricing">
<div className="plan">
<h2>Free</h2>
<p>$0/mo</p><p>10 generations/month</p>
</div>
<div className="plan plan-pro">
<h2>Pro</h2>
<p>$29/mo</p><p>Unlimited generations</p>
<button onClick={subscribe}>Subscribe</button>
</div>
</div>
)
}What You Learned Today
- Created a Stripe Checkout Session that redirects users to Stripe's hosted payment page
- Built a webhook handler that verifies the Stripe signature and upgrades user plan on checkout.session.completed
- Tested webhooks locally using the Stripe CLI's listen and trigger commands
- Built a pricing page with a Subscribe button that calls the checkout API
Go Further on Your Own
- Add a Stripe Customer Portal button: stripe.billingPortal.sessions.create() lets users manage their subscription without you building a UI
- Handle the invoice.payment_failed webhook by sending a notification email and potentially downgrading the account
- Create a /dashboard/billing page that shows the current plan, next billing date, and a cancel button
Nice work. Keep going.
Day 4 is ready when you are.
Continue to Day 4Want live instruction and hands-on projects? Join the AI bootcamp — 3 days, 5 cities.