| name | stripe-integration-expert |
| description | Especialista em Integração Stripe para implementar integrações Stripe em nível de produção: assinaturas com trials e proration, pagamentos únicos, billing baseado em uso, checkout sessions, handlers de webhook idempotentes, portal do cliente e faturamento. Cobre padrões Next.js, Express e Django. |
| agents | ["claude-code"] |
Especialista em Integração Stripe
Nível: AVANÇADO
Categoria: Equipe de Engenharia
Domínio: Pagamentos / Infraestrutura de Billing
Visão Geral
Implemente integrações Stripe em nível de produção: assinaturas com trials e proration, pagamentos únicos, billing baseado em uso, checkout sessions, handlers de webhook idempotentes, portal do cliente e faturamento. Cobre padrões Next.js, Express e Django.
Capacidades Principais
- Gerenciamento do ciclo de vida de assinatura (criar, fazer upgrade, downgrade, cancelar, pausar)
- Tratamento de trial e rastreamento de conversão
- Cálculo de proration e aplicação de crédito
- Billing baseado em uso com preços por medição
- Handlers de webhook idempotentes com verificação de assinatura
- Integração do portal do cliente
- Geração de fatura e acesso a PDF
- Configuração completa de testes locais com Stripe CLI
Quando Usar
- Adicionar billing de assinatura a qualquer aplicação web
- Implementar upgrades/downgrades de plano com proration
- Construir billing baseado em uso ou por assento
- Depurar falhas de entrega de webhook
- Migrar de um modelo de billing para outro
Máquina de Estado do Ciclo de Vida de Assinatura
FREE_TRIAL ──pago──► ACTIVE ──cancelar──► CANCEL_PENDING ──fim_período──► CANCELED
│ │ │
│ downgrade reativar
│ ▼ │
│ DOWNGRADING ──fim_período──► ACTIVE (plano inferior) │
│ │
└──fim_trial sem pagamento──► PAST_DUE ──pagamento_falhou 3x──► CANCELED
│
pagamento_sucesso
│
▼
ACTIVE
Valores de status de assinatura no DB:
trialing | active | past_due | canceled | cancel_pending | paused | unpaid
Configuração do Cliente Stripe
import Stripe from "stripe"
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-04-10",
typescript: true,
appInfo: {
name: "myapp",
version: "1.0.0",
},
})
export const PLANS = {
starter: {
monthly: process.env.STRIPE_STARTER_MONTHLY_PRICE_ID!,
yearly: process.env.STRIPE_STARTER_YEARLY_PRICE_ID!,
features: ["5 projetos", "10k eventos"],
},
pro: {
monthly: process.env.STRIPE_PRO_MONTHLY_PRICE_ID!,
yearly: process.env.STRIPE_PRO_YEARLY_PRICE_ID!,
features: ["Projetos ilimitados", "1M eventos"],
},
} as const
Checkout Session (Next.js App Router)
import { NextResponse } from "next/server"
import { stripe } from "@/lib/stripe"
import { getAuthUser } from "@/lib/auth"
import { db } from "@/lib/db"
export async function POST(req: Request) {
const user = await getAuthUser()
if (!user) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
const { priceId, interval = "monthly" } = await req.json()
let stripeCustomerId = user.stripeCustomerId
if (!stripeCustomerId) {
const customer = await stripe.customers.create({
email: user.email,
name: user.name,
metadata: { userId: user.id },
})
stripeCustomerId = customer.
db..({ : { : user. }, : { stripeCustomerId } })
}
session = stripe...({
: stripeCustomerId,
: ,
: [],
: [{ : priceId, : }],
: ,
: {
: user. ? : ,
: { : user. },
},
: ,
: ,
: { : user. },
})
.({ : session. })
}
Upgrade/Downgrade de Assinatura
export async function changeSubscriptionPlan(
subscriptionId: string,
newPriceId: string,
immediate = false
) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId)
const currentItem = subscription.items.data[0]
if (immediate) {
return stripe.subscriptions.update(subscriptionId, {
items: [{ id: currentItem.id, price: newPriceId }],
proration_behavior: "always_invoice",
billing_cycle_anchor: "unchanged",
})
} else {
return stripe.subscriptions.update(subscriptionId, {
items: [{ id: currentItem.id, price: newPriceId }],
proration_behavior: "none",
billing_cycle_anchor: "unchanged",
})
}
}
export async () {
subscription = stripe..(subscriptionId)
prorationDate = .(.() / )
invoice = stripe..({
: subscription. ,
: subscriptionId,
: [{ : subscription..[]., : newPriceId }],
: prorationDate,
})
{
: invoice.,
prorationDate,
: invoice..,
}
}
Handler de Webhook Completo (Idempotente)
import { NextResponse } from "next/server"
import { headers } from "next/headers"
import { stripe } from "@/lib/stripe"
import { db } from "@/lib/db"
import Stripe from "stripe"
async function hasProcessedEvent(eventId: string): Promise<boolean> {
const existing = await db.stripeEvent.findUnique({ where: { id: eventId } })
return !!existing
}
async function markEventProcessed(eventId: string, type: string) {
await db.stripeEvent.create({ data: { id: eventId, type, processedAt: new Date() } })
}
export async () {
body = req.()
signature = ().()!
: .
{
event = stripe..(body, signature, process..!)
} (err) {
.(, err)
.({ : }, { : })
}
( (event.)) {
.({ : , : })
}
{
(event.) {
:
(event.. ..)
:
:
(event.. .)
:
(event.. .)
:
(event.. .)
:
(event.. .)
:
.()
}
(event., event.)
.({ : })
} (err) {
.(, err)
.({ : }, { : })
}
}
() {
(session. !== )
userId = session.?.
(!userId) ()
subscription = stripe..(session. )
db..({
: { : userId },
: {
: session. ,
: subscription.,
: subscription..[]..,
: (subscription. * ),
: subscription.,
: ,
},
})
}
() {
user = db..({
: { : subscription. },
})
(!user) {
customer = db..({
: { : subscription. },
})
(!customer) ()
}
db..({
: { : subscription. },
: {
: subscription..[]..,
: (subscription. * ),
: subscription.,
: subscription.,
},
})
}
() {
db..({
: { : subscription. },
: {
: ,
: ,
: ,
: ,
},
})
}
() {
(!invoice.)
attemptCount = invoice.
db..({
: { : invoice. },
: { : },
})
(attemptCount >= ) {
(invoice.!, )
} {
(invoice.!, )
}
}
() {
(!invoice.)
db..({
: { : invoice. },
: {
: ,
: (invoice. * ),
},
})
}
Billing Baseado em Uso
export async function reportUsage(subscriptionItemId: string, quantity: number) {
await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
quantity,
timestamp: Math.floor(Date.now() / 1000),
action: "increment",
})
}
export async function trackApiCall(userId: string) {
const user = await db.user.findUnique({ where: { id: userId } })
if (user?.stripeSubscriptionId) {
const subscription = await stripe.subscriptions.retrieve(user.stripeSubscriptionId)
const meteredItem = subscription.items.data.find(
(item) => item.price.recurring?.usage_type ===
)
(meteredItem) {
(meteredItem., )
}
}
}
Portal do Cliente
import { NextResponse } from "next/server"
import { stripe } from "@/lib/stripe"
import { getAuthUser } from "@/lib/auth"
export async function POST() {
const user = await getAuthUser()
if (!user?.stripeCustomerId) {
return NextResponse.json({ error: "Sem conta de billing" }, { status: 400 })
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
})
return NextResponse.json({ url: portalSession.url })
}
Testes com Stripe CLI
brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failed
stripe trigger customer.subscription.updated \
--override subscription:customer=cus_xxx
stripe events list --limit 10
Helper de Controle de Acesso por Funcionalidade
export function isSubscriptionActive(user: { subscriptionStatus: string | null, stripeCurrentPeriodEnd: Date | null }) {
if (!user.subscriptionStatus) return false
if (user.subscriptionStatus === "active" || user.subscriptionStatus === "trialing") return true
if (user.subscriptionStatus === "past_due" && user.stripeCurrentPeriodEnd) {
return user.stripeCurrentPeriodEnd > new Date()
}
return false
}
export async function requireActiveSubscription() {
const user = await getAuthUser()
if (!isSubscriptionActive(user)) {
redirect("/billing?reason=subscription_required")
}
}
Armadilhas Comuns
- Ordem de entrega de webhooks não garantida — sempre re-busque da API Stripe, nunca confie nos dados do evento isoladamente para atualizações de DB
- Processamento duplo de webhooks — Stripe tenta novamente em 500; sempre use tabela de idempotência
- Rastreamento de conversão de trial — armazene
hasHadTrial: true no DB para prevenir abuso de trial
- Surpresas de proration — sempre pré-visualize proration antes do upgrade; mostre o valor ao usuário antes de confirmar
- Portal do cliente não configurado — deve habilitar funcionalidades no painel Stripe em Billing → Configurações do portal do cliente
- Metadados ausentes no checkout — sempre passe
userId em metadados; não é possível vincular assinatura ao usuário sem isso