How to Integrate Stripe Payments in Next.js?
Complete guide to adding Stripe subscriptions and one-time payments to your Next.js application

How to Integrate Stripe Payments in Next.js
Your app is built. Now make it make money.
Your App is Built. Now Make it Make Money.
You've spent weeks building your SaaS app. The UI is clean. The features work. Users love it.
But there's one problem.
You're not charging anyone.
Every day your app runs for free is a day you're leaving money on the table. And the #1 reason developers delay adding payments?
"It seems complicated."
It's not. I'll prove it to you in this guide.
Why Stripe?
Before we dive in — why Stripe over everything else?
| Feature | Stripe | PayPal | Razorpay |
|---|---|---|---|
| Developer Experience | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| Documentation | Best in class | Average | Good |
| Subscriptions | Very easy | Complex | Good |
| Webhooks | Simple | Complicated | Good |
| Global reach | ✅ | ✅ | India only |
| Test mode | ✅ | ✅ | ✅ |
Stripe wins on developer experience — every time.
What We're Building
By the end of this guide, you'll have:
- ✅ A working Stripe checkout page
- ✅ Subscription billing system
- ✅ Webhook handler for payment events
- ✅ User plan upgrade after payment
What You'll Need
- Next.js 14 project
- Stripe account — stripe.com (free)
- MongoDB — for storing user data
- Basic JavaScript knowledge
Step 1 — Install Stripe
npm install stripe @stripe/stripe-js
Add to .env.local:
STRIPE_SECRET_KEY=sk_test_your_secret_key
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
STRIPE_PRICE_ID=price_your_price_id
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
NEXT_PUBLIC_URL=http://localhost:3000
Step 2 — Create Stripe Instance
Create /lib/stripe.js:
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16'
})
export default stripe
Step 3 — Create Checkout Session
Create /app/api/create-checkout/route.js:
import stripe from '@/lib/stripe'
import { getServerSession } from 'next-auth'
import { authOptions } from '../auth/[...nextauth]/route'
export async function POST(req) {
// Get logged in user
const session = await getServerSession(authOptions)
if (!session) {
return Response.json(
{ error: 'Please login first' },
{ status: 401 }
)
}
try {
// Create Stripe checkout session
const checkoutSession = await stripe.checkout.sessions.create({
mode: 'subscription',
customer_email: session.user.email,
payment_method_types: ['card'],
line_items: [
{
price: process.env.STRIPE_PRICE_ID,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
metadata: {
userId: session.user.id,
}
})
return Response.json({ url: checkoutSession.url })
} catch (error) {
return Response.json(
{ error: error.message },
{ status: 500 }
)
}
}
Step 4 — Add Checkout Button
Create your pricing page component:
// /app/pricing/page.jsx
'use client'
import { useState } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
export default function PricingPage() {
const { data: session } = useSession()
const router = useRouter()
const [loading, setLoading] = useState(false)
const handleCheckout = async () => {
if (!session) {
router.push('/login')
return
}
setLoading(true)
try {
const res = await fetch('/api/create-checkout', {
method: 'POST',
})
const data = await res.json()
if (data.url) {
window.location.href = data.url
}
} catch (error) {
console.error('Checkout error:', error)
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="bg-white rounded-2xl p-8 shadow-lg max-w-sm w-full">
<h2 className="text-2xl font-bold mb-2">Pro Plan</h2>
<p className="text-gray-500 mb-6">Everything you need to scale</p>
<div className="text-4xl font-bold mb-6">
$29<span className="text-lg text-gray-400">/month</span>
</div>
<ul className="space-y-3 mb-8">
<li>✅ Unlimited projects</li>
<li>✅ Priority support</li>
<li>✅ Advanced analytics</li>
<li>✅ Custom domain</li>
</ul>
<button
onClick={handleCheckout}
disabled={loading}
className="w-full bg-black text-white py-3 rounded-xl font-medium hover:bg-gray-800 transition-all"
>
{loading ? 'Loading...' : 'Get Started →'}
</button>
</div>
</div>
)
}
Step 5 — Handle Webhooks
This is the most important step. Webhooks tell your app what happened after payment.
Create /app/api/webhooks/stripe/route.js:
import stripe from '@/lib/stripe'
import connectDB from '@/lib/mongodb'
import User from '@/models/User'
export async function POST(req) {
const body = await req.text()
const sig = req.headers.get('stripe-signature')
let event
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
)
} catch (err) {
console.error('Webhook signature failed:', err)
return Response.json(
{ error: 'Webhook error' },
{ status: 400 }
)
}
await connectDB()
switch (event.type) {
// Payment successful → upgrade user
case 'checkout.session.completed':
const session = event.data.object
await User.findOneAndUpdate(
{ email: session.customer_email },
{
isPro: true,
stripeCustomerId: session.customer,
subscriptionId: session.subscription
}
)
console.log('✅ User upgraded to Pro:', session.customer_email)
break
// Subscription cancelled → downgrade user
case 'customer.subscription.deleted':
const subscription = event.data.object
await User.findOneAndUpdate(
{ subscriptionId: subscription.id },
{ isPro: false }
)
console.log('❌ Subscription cancelled')
break
// Payment failed → notify user
case 'invoice.payment_failed':
console.log('⚠️ Payment failed:', event.data.object.customer_email)
break
}
return Response.json({ received: true })
}
Step 6 — Test Your Integration
Stripe gives you test card numbers — no real money involved:
Card Number → 4242 4242 4242 4242
Expiry → Any future date (e.g., 12/26)
CVC → Any 3 digits (e.g., 123)
Test the full flow:
1. Click "Get Started" on pricing page
2. Enter test card details
3. Complete payment
4. Check your database — user should be isPro: true
5. Check Stripe dashboard — payment should appear
Step 7 — Protect Pro Features
Now that you know who's a Pro user — protect your premium features:
// /app/api/premium-feature/route.js
import { getServerSession } from 'next-auth'
import connectDB from '@/lib/mongodb'
import User from '@/models/User'
export async function GET(req) {
const session = await getServerSession(authOptions)
if (!session) {
return Response.json({ error: 'Not logged in' }, { status: 401 })
}
await connectDB()
const user = await User.findOne({ email: session.user.email })
if (!user?.isPro) {
return Response.json(
{ error: 'Upgrade to Pro to access this feature' },
{ status: 403 }
)
}
// Return premium content
return Response.json({ data: 'Premium content here' })
}
Common Mistakes to Avoid
❌ Using live keys in development — always use test keys
❌ Not verifying webhook signatures — security risk
❌ Forgetting to handle subscription cancellation
❌ Not testing with Stripe test cards
❌ Storing card details yourself — let Stripe handle it
The Bigger Picture
Adding Stripe to your app isn't just a technical task. It's a mindset shift.
The moment you add a payment button — your side project becomes a business.
Before Stripe → "Cool project"
After Stripe → "Real business"
Your skills have value. Start charging for them.
What's Next?
Once payments are working, here's what to add:
- 📧 Payment confirmation emails via Resend
- 📊 Revenue dashboard — track MRR, churn
- 🔄 Annual billing — offer discount for yearly plans
- 🎁 Free trial — 14 days before charging
- 💳 Customer portal — let users manage their subscription
Building a SaaS? Have questions about Stripe? Drop a comment — happy to help.




