๐ณ Stripe Subscriptions in Action: Building Recurring Revenue
In previous chapters, we learned how to implement a "One-time Payment" checkout flow.
But if you're building a SaaS platform or a paid membership community, Monthly Recurring Revenue (MRR) is the key to financial freedom!
This chapter will guide you through the complete setup of Stripe Subscriptions, including creating subscription plans, initiating Checkout Sessions, and most importantly: how to listen for successful automatic monthly payments via Webhooks.
1. What are Stripe Subscriptions?
The key difference between Stripe Subscriptions and one-time payments lies in their "recurring" nature.
- One-time Payments (Payment Intents): Charge once, done.
- Subscriptions: After customers bind their credit cards, Stripe automatically charges them on the same day each month until they actively cancel.
Core Components of Subscriptions:
- Product: e.g., "Vibe Tutor Premium Membership".
- Price: Not just a number, but an object with a "Recurring" attribute, e.g., "$299/month".
- Customer: Subscriptions require a Customer object to bind credit cards and contact information.
- Subscription: The contract binding a Customer to a Price.
2. Create Subscription Plans in the Stripe Dashboard
Before writing code, the easiest way is to create products and prices directly in the Stripe Dashboard.
- Log in to the Stripe Dashboard, click Products -> Add Product.
- Enter the Name:
Vibe Tutor Premium Membership. - Select Pricing model:
Standard pricing. - Enter Price:
299. - Select Billing period:
Monthly. - Click Save product.
After saving, a Price ID starting with price_ (e.g., price_1Nxyz...) will appear in the price list.
Copy this Price IDโitโs the most important key for our code!
3. Initiate a Subscription Checkout Session
With the Price ID, weโll create an API in Next.js to generate a checkout URL.
This step is very similar to one-time payments, but with two key differences:
modemust be set to'subscription'.- We must pass
customer_emailor create acustomer, as subscriptions need to know who is paying.
Writing the API Route
Create the file: src/app/api/stripe/create-subscription/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
// Initialize Stripe (remember to store the Secret Key in .env.local)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
export async function POST(request: Request) {
try {
const body = await request.json();
const { email, userId } = body;
// 1. Validate parameters
if (!email || !userId) {
return NextResponse.json({ error: 'Missing email or userId' }, { status: 400 });
}
// 2. Create Checkout Session
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'subscription', // Key 1: This is a subscription!
customer_email: email, // Key 2: Email is required to bind the customer
line_items: [
{
// Paste the Price ID copied earlier here
price: 'price_1NxyzYOUR_PRICE_ID_HERE',
quantity: 1,
},
],
// Redirect URLs for success and cancellation
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
// Key 3: Pass our database User ID into metadata so the Webhook knows who paid
subscription_data: {
metadata: {
vibe_user_id: userId,
},
},
});
// Return the generated checkout URL
return NextResponse.json({ url: session.url });
} catch (error: any) {
console.error('Stripe Subscription Error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
4. Listen for Subscription Status via Webhook
After a customer successfully completes checkout, Stripe will automatically charge them each month.
We need a Webhook to receive Stripe notifications and update VIP status in our database.
For subscriptions, the three most critical events to listen for are:
checkout.session.completed: The customerโs first successful payment and card binding.invoice.payment_succeeded: Successful automatic monthly charges! (The essence of MRR).customer.subscription.deleted: The customer cancels or their credit card fails permanently.
Implementing the Webhook Logic
Create/modify the file: src/app/api/stripe/webhook/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { headers } from 'next/headers';
// Assume you have your own database update logic
import { updateUserSubscription } from '@/lib/db';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text();
const sig = headers().get('stripe-signature');
let event: Stripe.Event;
try {
// Verify this is a genuine Stripe call, not a hacker forgery
event = stripe.webhooks.constructEvent(body, sig!, endpointSecret);
} catch (err: any) {
return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
}
// Handle different events
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
if (session.mode === 'subscription') {
// First subscription success
const userId = session.subscription_data?.metadata?.vibe_user_id;
const subscriptionId = session.subscription as string;
console.log(`User ${userId} started subscription ${subscriptionId}`);
// TODO: Save subscriptionId to the database and grant VIP access
await updateUserSubscription(userId, 'active', subscriptionId);
}
break;
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object as Stripe.Invoice;
if (invoice.subscription) {
// Monthly payment succeeded! Renewal!
const subscriptionId = invoice.subscription as string;
console.log(`Payment succeeded for subscription ${subscriptionId}`);
// TODO: Update the subscription expiry date in the database (extend by one month)
}
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
const subscriptionId = subscription.id;
console.log(`Subscription ${subscriptionId} was canceled.`);
// TODO: Revoke the userโs VIP access
break;
}
default:
console.log(`Unhandled event type ${event.type}`);
}
return NextResponse.json({ received: true });
}
5. Let Customers Manage Their Subscriptions (Customer Portal)
The biggest headache with subscriptions is "support overload": daily emails like "I need to update my card," "I want to cancel," or "I need an invoice."
One of Stripeโs most generous features is its built-in, completely free Customer Portal.
With just one line of code, you can generate a dedicated URL where customers can:
- Update credit card details
- Pause or cancel subscriptions
- Download past receipts and invoices
Generate a Customer Portal URL
// src/app/api/stripe/portal/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
export async function POST(request: Request) {
try {
const { customerId } = await request.json(); // Stripe Customer ID from your database
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard`, // Where to redirect after management
});
return NextResponse.json({ url: session.url });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
All you need is a frontend button like <button onClick={handleManageBilling}>Manage My Subscription</button> to call this API, get the URL, and redirect. Your support costs drop to zero!
This is the beauty of SaaS platforms: write the code once, and the system works like a tireless robot, automatically collecting payments, issuing invoices, and handling cancellations every month. Ready to welcome your first MRR? ๐