# How to Integrate Stripe Payments in Next.js?

# 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

```bash
npm install stripe @stripe/stripe-js
```

Add to `.env.local`:

```bash
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`:

```javascript
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`:

```javascript
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:

```jsx
// /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`:

```javascript
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:

```javascript
// /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.*

