Back to articles

How to Build a Stripe Checkout in Next.js with Subscription Tiers

A step-by-step guide to adding Pro and Premium plans with secure billing. If you’re building a SaaS, marketplace, or gated content platform, adding Stripe Checkout for subscription payments is one of the most powerful ways to monetize. It’s fast, secure, and scales with your business.

Category

Design

Author

Aleksi Huusko

Published

18.7.2025

Updated

25.7.2025

Share

Urban Flux Through a Window

Summary

  • Stripe Checkout simplifies secure billing — You can quickly set up Pro and Premium subscription tiers using Stripe’s hosted checkout flow and Price IDs.
  • Next.js API routes handle subscription logic — A POST endpoint securely creates a checkout session and redirects the user based on success or cancellation.
  • Frontend integrates easily with dynamic buttons — Call the API route with the selected tier’s price ID and redirect to the Stripe-hosted checkout page.
  • Webhooks and portals enhance subscription management — Use Stripe webhooks to track user status and optionally add a customer billing portal for self-service.

In this tutorial, I’ll show you how to build a Stripe-powered subscription flow in a Next.js project—with two tiers: Pro and Premium. We’ll cover Stripe setup, secure API routes, subscription logic, and basic frontend integration.

identity
This is the copyright label.

Whether you’re building your first paid product or adding billing to an existing app, this will get you there.

What You’ll Need:

  • Basic knowledge of Next.js (App Router or Pages Router)
  • A Stripe account (create one here)
  • Your Stripe secret and publishable keys
  • Optional: A database (like Supabase) for managing users and subscriptions

1. Set Up Stripe Products & Prices

Start by creating two products in your Stripe Dashboard.

Pro Plan

  • Price: €150/month
  • Product ID: prod_PRO
  • Price ID: price_PRO

Premium Plan

  • Price: €500/month
  • Product ID: prod_PREMIUM
  • Price ID: price_PREMIUM

Stripe Checkout uses Price IDs, not Product IDs, for billing sessions.

2. Add Stripe Keys to .env

In your Next.js root directory:

STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO=price_PRO
NEXT_PUBLIC_STRIPE_PRICE_ID_PREMIUM=price_PREMIUM

Make sure .env.local is loaded with dotenv or Next’s default behavior.

3. Install Stripe SDK

npm install stripe

If you’re using TypeScript:

npm install --save-dev @types/stripe

4. Create a Checkout API Route

In /app/api/checkout/route.ts (or /pages/api/checkout.ts if using Pages Router):

import { NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-08-16',
});

export async function POST(req: Request) {
const { priceId, userEmail } = await req.json();

try {
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
customer_email: userEmail,
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
});

return NextResponse.json({ url: session.url });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}

Make sure your userEmail is validated or retrieved from your auth context.

5. Create a Pricing Page with Checkout Buttons

// app/pricing/page.tsx
'use client';
import { useState } from 'react';

export default function PricingPage() {
const [loading, setLoading] = useState(false);

const handleCheckout = async (priceId: string) => {
setLoading(true);
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ priceId, userEmail: 'user@example.com' }),
});

const data = await res.json();
if (data.url) window.location.href = data.url;
setLoading(false);
};

return (
<div className="space-y-6 p-8 max-w-xl mx-auto text-center">
<h1 className="text-3xl font-bold">Choose Your Plan</h1>

<div className="grid grid-cols-1 gap-6">
<button
onClick={() => handleCheckout(process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO!)}
disabled={loading}
className="bg-black text-white px-6 py-3 rounded-md"
>
{loading ? 'Redirecting…' : 'Subscribe to Pro – €150/month'}
</button>

<button
onClick={() => handleCheckout(process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PREMIUM!)}
disabled={loading}
className="bg-gray-800 text-white px-6 py-3 rounded-md"
>
{loading ? 'Redirecting…' : 'Subscribe to Premium – €500/month'}
</button>
</div>
</div>
);
}

6. (Optional) Add Webhooks to Store Subscription Data

To track active subscriptions, you should configure a Stripe webhook to listen for events like checkout.session.completed or invoice.paid. You can store the status in your database (e.g., Supabase or PostgreSQL) and gate content based on it.

Stripe CLI can help for local testing:

stripe listen --forward-to localhost:3000/api/webhook

7. Final Testing

  • Try both subscription tiers
  • Use Stripe test cards
  • Check the Checkout experience, redirection, and Stripe Dashboard

Bonus Tips

  • Use Stripe Customer Portal for billing management (cancellations, invoices)
  • Protect premium routes by checking subscription status server-side
  • Create a /success page with onboarding or account setup steps

Conclusion

Stripe Checkout makes adding subscription billing to your Next.js app surprisingly smooth—especially with the App Router and modern API routes.

You now have a working subscription system that:

  • Handles secure payments
  • Redirects users on success/cancel
  • Can be extended with webhooks and portals
  • Supports multiple pricing tiers

Got an idea you’d like to bounce around?