Back to Knowledge
PlaybookPayments

Subscription Models

Subscriptions are recurring payments—like a gym membership for your app. Users pay monthly or yearly, you get predictable revenue. Stripe handles the complexity.

How Subscriptions Work

Think of it like Netflix: user signs up → picks a plan → enters payment → gets billed automatically every month. Stripe manages the entire lifecycle:

Checkout

User picks plan & pays

Billing Cycle

Auto-charged monthly/yearly

Recurring Revenue

Predictable income

Stripe Subscription Concepts

Products

What you're selling. Example: "SaucyTech Builder" plan. Created once in Stripe Dashboard.

Prices

How much and how often. One product can have multiple prices: $19/month and $149/year. Each price has a unique price_id.

Subscriptions

The actual billing relationship. Links a Customer to a Price. Has status: active, canceled, past_due, etc.

Customers

Stripe's record of a paying user. Store the stripe_customer_id in your database linked to your user.

The Subscription Flow

1

User clicks "Subscribe"

Your app creates a Stripe Checkout Session with the price_id.

2

Stripe Checkout handles payment

User enters card details on Stripe's hosted page. You never touch card numbers.

3

Webhook confirms success

Stripe sends checkout.session.completed. You update the user's subscription status.

4

Access granted

Check subscription_status === "active" before showing premium features.

5

Auto-renewal

Stripe charges automatically each cycle. Webhooks notify you of success or failure.

Creating a Checkout Session

// app/api/stripe/checkout/route.ts

import

Stripe

from

"stripe"

;

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {

const { priceId, userId } = await req.json();

const session = await stripe.checkout.sessions.create({

mode: "subscription",

payment_method_types: ["card"],

line_items: [{

price: priceId,

quantity: 1,

}],

success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,

cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,

metadata: { userId }, // Link to your user!

});

return Response.json({ url: session.url });

}

The metadata trick

Pass your userId in metadata. When the webhook fires, you'll know which user to update.

Upgrades & Downgrades

Upgrade

Monthly → Yearly. Stripe prorates: user pays the difference immediately.

Downgrade

Yearly → Monthly. User keeps access until period ends, then new price kicks in.

// Upgrade a subscription to a new price

await stripe.subscriptions.update(subscriptionId, {

items: [{

id: existingItemId,

price: newPriceId,

}],

proration_behavior: "create_prorations",

});

Trial Periods

Let users try before they buy. Stripe won't charge until the trial ends.

// Add a 7-day trial to checkout

const session = await stripe.checkout.sessions.create({

mode: "subscription",

subscription_data: {

trial_period_days: 7,

},

// ... rest of config

});

Trial Status

During trial, subscription status is trialing. Check for both active and trialing to grant access.

Common Pitfalls

Checking payment at checkout only

Subscriptions can fail, cancel, or expire. Checking only at signup misses changes.

Fix: Listen to webhooks for subscription changes. Always check current status before granting access.

Not handling failed payments

Cards expire, get declined, or hit limits. Subscription goes past_due.

Fix: Listen for invoice.payment_failed. Email the user. Stripe auto-retries, but you should notify them.

Proration surprises

User upgrades mid-cycle and gets charged unexpectedly (or you lose money on downgrades).

Fix: Understand proration. Show users what they'll pay. Use proration_behavior to control timing.

Using test keys in production

sk_test_ keys don't process real payments.

Fix: Use sk_live_ in production env vars. Double-check before launch!

Handle subscription events

Webhooks tell you when subscriptions change. Learn to handle them reliably.

Webhooks Guide →