Skip to main content

Command Palette

Search for a command to run...

How to Integrate Stripe Payments in Next.js?

Complete guide to adding Stripe subscriptions and one-time payments to your Next.js application

Updated
6 min read
How to Integrate Stripe Payments in Next.js?
N
Hi there! I'm a Full-Stack Developer and AI Automation Engineer crafting innovative web applications and automation workflows that saves time, reduces effort, scale your growth, and drive real results.

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.