Search across all documentation pages
Create subscription products and prices in Stripe, then use Checkout Sessions or PaymentIntents to subscribe customers. Manage subscription lifecycle with the Customer Portal and webhooks.
Create a subscription via Checkout Session:
// app/actions/subscribe.ts
"use server";
import { stripe } from "@/lib/stripe";
import { redirect } from "next/navigation";
export async function createSubscriptionCheckout(priceId: string) {
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
subscription_data: {
trial_period_days: 14,
},
});
redirect(session.url!);
}Or create a subscription directly via the API:
// app/actions/subscribe-direct.ts
"use server";
import { stripe } from "@/lib/stripe";
export async function createSubscription(
customerId: string,
priceId: string
) {
const subscription = await stripe.subscriptions.create({
// app/pricing/page.tsx
import { stripe } from "@/lib/stripe";
import { createSubscriptionCheckout } from "@/app/actions/subscribe";
interface PricingTier {
name: string;
priceId: string;
price: number;
interval:
interval (day, week, month, year) and an amount.mode: "subscription", Stripe creates the Customer, Subscription, and handles the first payment automatically.payment_behavior: "default_incomplete", the subscription starts in an incomplete state until the first payment is confirmed client-side.trialing (if trial set) then active, past_due (payment failed), canceled, or unpaid.trialing and no invoice is generated until the trial ends.Annual vs monthly pricing:
// Create both prices for the same product
const monthlyPrice = await stripe.prices.create({
product: "prod_xxx",
unit_amount: 2900,
currency: "usd",
recurring: { interval: "month" },
});
const annualPrice = await stripe.prices.create({
Cancel a subscription:
// Cancel at end of billing period
await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
// Cancel immediately
await stripe.subscriptions.cancel(subscriptionId);Update subscription (change plan):
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
await stripe.subscriptions.update(subscriptionId, {
items: [
{
id: subscription.items.data[0].id,
price: newPriceId,
},
],
proration_behavior: "create_prorations",
});stripe.subscriptions.create returns Promise<Stripe.Subscription>.expand, the expanded fields change types. Cast explicitly after expansion."active" | "past_due" | "canceled" | "incomplete" | "incomplete_expired" | "trialing" | "unpaid" | "paused".import type Stripe from "stripe";
function isActive(subscription: Stripe.Subscription): boolean {
return subscription.status === "active" || subscription.status === "trialing";
}customer.subscription.created, invoice.paid).payment_behavior: "default_incomplete", you must confirm the PaymentIntent client-side or the subscription stays incomplete and expires after 23 hours.proration_behavior: "none" to disable it.cancel_at_period_end: true does not immediately cancel. The subscription stays active until the current period ends. Listen for customer.subscription.deleted to revoke access.payment_method_collection: "if_required" if you do not want to collect a card upfront. The default collects a card.| Approach | Pros | Cons |
|---|---|---|
| Checkout Session (subscription mode) | Minimal code, handles everything | Limited UI customization |
| API-created subscription + PaymentElement | Full UI control, in-app checkout | More complex setup |
| Payment Links | Zero-code subscription setup | No programmatic control |
| Customer Portal for plan changes | Stripe-hosted management UI | Less integrated feel |
trialing (if trial set) -> active -> past_due (payment failed) -> canceled or unpaid. The full union is: "active" | "past_due" | "canceled" | "incomplete" | "incomplete_expired" | "trialing" | "unpaid" | "paused".
It starts the subscription in an incomplete state, requiring the client to confirm the first PaymentIntent. If not confirmed within 23 hours, the subscription expires automatically.
// End of period (subscription stays active until period ends)
await stripe.subscriptions.update(subId, {
cancel_at_period_end: true,
});
// Immediately
await stripe.subscriptions.cancel(subId);Users can manually navigate to your success URL. Always verify subscription status via webhooks (customer.subscription.created, invoice.paid) or by retrieving the session/subscription server-side.
By default, Stripe creates a prorated credit for the remaining time on the old plan and charges the difference for the new plan. Set proration_behavior: "none" to disable this.
await stripe.prices.create({
product: "prod_xxx",
unit_amount: 2900,
currency: "usd",
recurring: { interval: "month" },
});
await stripe.prices.create({
product: "prod_xxx",
unit_amount: 29000,
No. The subscription stays active until the current billing period ends. The user retains access during that time. Listen for customer.subscription.deleted to revoke access when it finally cancels.
trialingpayment_method_collection: "if_required" to skip card collectionimport type Stripe from "stripe";
function isActive(sub: Stripe.Subscription): boolean {
return sub.status === "active" || sub.status === "trialing";
}When you use expand: ["latest_invoice.payment_intent"], the expanded objects are typed as their ID string by default. You must cast them to the actual types:
const invoice = subscription.latest_invoice as Stripe.Invoice;
const pi = invoice.payment_intent as Stripe.PaymentIntent;