Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Crie produtos e preços de assinatura no Stripe, em seguida, use Checkout Sessions ou PaymentIntents para assinar clientes. Gerencie o ciclo de vida da assinatura com o Portal do Cliente e webhooks.
Crie uma assinatura 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!);
}Ou crie uma assinatura diretamente via 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 projects", "1 GB storage", "Email support"],
},
{
name: "Pro",
priceId: "price_pro_monthly",
price: 29,
interval: "month",
features: ["Unlimited projects", "10 GB storage", "Priority support", "API access"],
popular: true,
},
{
name: "Enterprise",
priceId: "price_enterprise_monthly",
price: 99,
interval: "month",
features: [
"Unlimited everything",
"100 GB storage",
"Dedicated support",
"SSO",
"Custom integrations",
],
},
];
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">Pricing</h1>
<p className="text-gray-600 text-center mb-12">
Start with a 14-day free trial. No credit card required.
</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">
Most 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"
}`}
>
Start Free Trial
</button>
</form>
</div>
))}
</div>
</div>
);
}interval (dia, semana, mês, ano) e um amount.mode: "subscription", o Stripe cria o Cliente, a Assinatura e lida com o primeiro pagamento automaticamente.payment_behavior: "default_incomplete", a assinatura começa em um estado incomplete até que o primeiro pagamento seja confirmado no lado do cliente.trialing (se trial definido) então active, past_due (pagamento falhou), canceled, ou unpaid.trialing e nenhuma fatura é gerada até que o trial termine.Preços anuais vs. mensais:
// Crie ambos os preços para o mesmo produto
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/mês -- desconto anual
currency: "usd",
recurring: { interval: "year" },
});Cancelar uma assinatura:
// Cancelar no final do período de faturamento
await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
// Cancelar imediatamente
await stripe.subscriptions.cancel(subscriptionId);Atualizar assinatura (mudar plano):
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 retorna Promise<Stripe.Subscription>.expand, os campos expandidos mudam de tipo. Faça o cast explicitamente após a expansão."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", você deve confirmar o PaymentIntent no lado do cliente ou a assinatura permanecerá incomplete e expirará após 23 horas.proration_behavior: "none" para desativá-la.cancel_at_period_end: true não cancela imediatamente. A assinatura permanece ativa até o final do período atual. Ouça customer.subscription.deleted para revogar o acesso.payment_method_collection: "if_required" se você não quiser coletar um cartão antecipadamente. O padrão coleta um cartão.| Abordagem | Prós | Contras |
|---|---|---|
| Checkout Session (modo de assinatura) | Código mínimo, lida com tudo | Personalização limitada da UI |
| Assinatura criada via API + PaymentElement | Controle total da UI, checkout no aplicativo | Configuração mais complexa |
| Payment Links | Configuração de assinatura sem código | Sem controle programático |
| Customer Portal para mudanças de plano | UI de gerenciamento hospedada pelo Stripe | Sensação menos integrada |
trialing (se trial definido) -> active -> past_due (pagamento falhou) -> canceled ou unpaid. A união completa é: "active" | "past_due" | "canceled" | "incomplete" | "incomplete_expired" | "trialing" | "unpaid" | "paused".
Inicia a assinatura em um estado incomplete, exigindo que o cliente confirme o primeiro PaymentIntent. Se não for confirmado em 23 horas, a assinatura expira automaticamente.
// Fim do período (a assinatura permanece ativa até o fim do período)
await stripe.subscriptions.update(subId, {
cancel_at_period_end: true,
});
// Imediatamente
await stripe.subscriptions.cancel(subId);Os usuários podem navegar manualmente para sua URL de sucesso. Sempre verifique o status da assinatura via webhooks (customer.subscription.created, invoice.paid) ou recuperando a sessão/assinatura no lado do servidor.
Por padrão, o Stripe cria um crédito prorated pelo tempo restante no plano antigo e cobra a diferença para o novo plano. Defina proration_behavior: "none" para desativar isso.
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" },
});Não. A assinatura permanece active até o final do período de faturamento atual. O usuário mantém o acesso durante esse tempo. Ouça customer.subscription.deleted para revogar o acesso quando ela finalmente for cancelada.
trialingpayment_method_collection: "if_required" para pular a coleta do cartãoimport type Stripe from "stripe";
function isActive(sub: Stripe.Subscription): boolean {
return sub.status === "active" || sub.status === "trialing";
}Quando você usa expand: ["latest_invoice.payment_intent"], os objetos expandidos são tipados como sua string de ID por padrão. Você precisa fazer o cast deles para os tipos reais:
const invoice = subscription.latest_invoice as Stripe.Invoice;
const pi = invoice.payment_intent as Stripe.PaymentIntent;Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥