Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Crea productos y precios de suscripción en Stripe, luego usa Checkout Sessions o PaymentIntents para suscribir clientes. Gestiona el ciclo de vida de la suscripción con el Customer Portal y los webhooks.
Crea una suscripción mediante una 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!);
}O crea una suscripción directamente mediante la 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({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: "default_incomplete",
payment_settings: {
save_default_payment_method: "on_subscription",
},
expand: ["latest_invoice.payment_intent"],
});
const invoice = subscription.latest_invoice as Stripe.Invoice;
const paymentIntent = invoice.payment_intent as Stripe.PaymentIntent;
return {
subscriptionId: subscription.id,
clientSecret: paymentIntent.client_secret!,
};
}// app/pricing/page.tsx
import { stripe } from "@/lib/stripe";
import { createSubscriptionCheckout } from "@/app/actions/subscribe";
interface PricingTier {
name: string;
priceId: string;
price: number;
interval: string;
features: string[];
popular?: boolean;
}
const tiers: PricingTier[] = [
{
name: "Starter",
priceId: "price_starter_monthly",
price: 9,
interval: "month",
features: ["5 proyectos", "1 GB de almacenamiento", "Soporte por email"],
},
{
name: "Pro",
priceId: "price_pro_monthly",
price: 29,
interval: "month",
features: ["Proyectos ilimitados", "10 GB de almacenamiento", "Soporte prioritario", "Acceso a la API"],
popular: true,
},
{
name: "Enterprise",
priceId: "price_enterprise_monthly",
price: 99,
interval: "month",
features: [
"Todo ilimitado",
"100 GB de almacenamiento",
"Soporte dedicado",
"SSO",
"Integraciones personalizadas",
],
},
];
export default function PricingPage() {
return (
<div className="max-w-5xl mx-auto py-16 px-4">
<h1 className="text-4xl font-bold text-center mb-4">Precios</h1>
<p className="text-gray-600 text-center mb-12">
Comienza con una prueba gratuita de 14 días. No se requiere tarjeta de crédito.
</p>
<div className="grid md:grid-cols-3 gap-8">
{tiers.map((tier) => (
<div
key={tier.name}
className={`rounded-xl border p-8 ${
tier.popular
? "border-blue-500 ring-2 ring-blue-500 relative"
: "border-gray-200"
}`}
>
{tier.popular && (
<span className="absolute -top-3 left-1/2 -translate-x-1/2 bg-blue-500 text-white text-xs px-3 py-1 rounded-full">
Más popular
</span>
)}
<h2 className="text-xl font-bold">{tier.name}</h2>
<p className="mt-4">
<span className="text-4xl font-bold">${tier.price}</span>
<span className="text-gray-500">/{tier.interval}</span>
</p>
<ul className="mt-6 space-y-3">
{tier.features.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<span className="text-green-500">✓</span>
{feature}
</li>
))}
</ul>
<form
action={createSubscriptionCheckout.bind(null, tier.priceId)}
className="mt-8"
>
<button
type="submit"
className={`w-full py-3 rounded-lg font-medium ${
tier.popular
? "bg-blue-600 text-white hover:bg-blue-700"
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
}`}
>
Iniciar prueba gratuita
</button>
</form>
</div>
))}
</div>
</div>
);
}interval (day, week, month, year) y un amount.mode: "subscription", Stripe crea el Customer, la Subscription y gestiona el primer pago automáticamente.payment_behavior: "default_incomplete", la suscripción comienza en estado incomplete hasta que se confirme el primer pago en el cliente.trialing (si hay prueba configurada), luego active, past_due (pago fallido), canceled o unpaid.trialing y no se genera ninguna factura hasta que termina la prueba.Precios anuales frente a mensuales:
// Crea ambos precios para el mismo producto
const monthlyPrice = await stripe.prices.create({
product: "prod_xxx",
unit_amount: 2900,
currency: "usd",
recurring: { interval: "month" },
});
const annualPrice = await stripe.prices.create({
product: "prod_xxx",
unit_amount: 29000, // ~$241/mes - descuento anual
currency: "usd",
recurring: { interval: "year" },
});Cancelar una suscripción:
// Cancelar al final del periodo de facturación
await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
// Cancelar inmediatamente
await stripe.subscriptions.cancel(subscriptionId);Actualizar suscripción (cambiar de 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 devuelve Promise<Stripe.Subscription>.expand, los campos expandidos cambian de tipo. Haz un cast explícito después de la expansión."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", debes confirmar el PaymentIntent en el cliente; de lo contrario, la suscripción permanece incomplete y expira después de 23 horas.proration_behavior: "none" para desactivarlo.cancel_at_period_end: true no cancela de inmediato. La suscripción permanece active hasta que termine el periodo actual. Escucha customer.subscription.deleted para revocar el acceso.payment_method_collection: "if_required" si no quieres solicitar una tarjeta por adelantado. El valor predeterminado sí la solicita.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Sesión de Checkout (modo subscription) | Código mínimo, lo gestiona todo | Personalización limitada de la UI |
| Suscripción creada por API + PaymentElement | Control total de la UI, checkout en la app | Configuración más compleja |
| Payment Links | Configuración de suscripción sin código | Sin control programático |
| Customer Portal para cambios de plan | UI de gestión alojada por Stripe | Sensación menos integrada |
trialing (si hay prueba configurada) -> active -> past_due (pago fallido) -> canceled o unpaid. La unión completa es: "active" | "past_due" | "canceled" | "incomplete" | "incomplete_expired" | "trialing" | "unpaid" | "paused".
Inicia la suscripción en estado incomplete, lo que requiere que el cliente confirme el primer PaymentIntent. Si no se confirma en un plazo de 23 horas, la suscripción expira automáticamente.
// Fin de periodo (la suscripción permanece active hasta que termine el periodo)
await stripe.subscriptions.update(subId, {
cancel_at_period_end: true,
});
// Inmediatamente
await stripe.subscriptions.cancel(subId);Los usuarios pueden navegar manualmente a tu URL de éxito. Verifica siempre el estado de la suscripción mediante webhooks (customer.subscription.created, invoice.paid) o recuperando la sesión/suscripción en el servidor.
Por defecto, Stripe crea un crédito prorrateado por el tiempo restante del plan anterior y cobra la diferencia del nuevo plan. Establece proration_behavior: "none" para desactivarlo.
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,
currency: "usd",
recurring: { interval: "year" },
});No. La suscripción permanece active hasta que termine el periodo de facturación actual. El usuario conserva el acceso durante ese tiempo. Escucha customer.subscription.deleted para revocar el acceso cuando finalmente se cancele.
trialingpayment_method_collection: "if_required" para omitir la recogida de tarjetaimport type Stripe from "stripe";
function isActive(sub: Stripe.Subscription): boolean {
return sub.status === "active" || sub.status === "trialing";
}Cuando usas expand: ["latest_invoice.payment_intent"], los objetos expandidos están tipados como su cadena de ID por defecto. Debes hacerles cast a los tipos reales:
const invoice = subscription.latest_invoice as Stripe.Invoice;
const pi = invoice.payment_intent as Stripe.PaymentIntent;Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥