Setup
1. Stripe Dashboard
- https://dashboard.stripe.com > Developers > API Keys > copie Secret Key
- Webhooks > Add endpoint > URL:
https://[tenant].studeia.com/api/webhooks/stripe - Events to send:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.payment_failed
- Copie Webhook signing secret
2. Criar produtos + prices
Para cada plano B2B do Studeia (mini, growth, pro_100):
- Products > Add product > nome (ex: "Studeia Mini")
- Pricing: Recurring monthly + BRL R$ 250.00
- Copie price_id (ex:
price_1TZk...) - Repita para USD se quiser suportar clientes internacionais
3. Env vars
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_MINI=price_...
STRIPE_PRICE_GROWTH=price_...
STRIPE_PRICE_PRO_100=price_...
STRIPE_PRICE_MINI_USD=price_...
STRIPE_PRICE_GROWTH_USD=price_...
STRIPE_PRICE_PRO_100_USD=price_...
Fluxo de checkout
Admin clica "Contratar Mini" em /institution/billing
↓
POST /api/institution/billing/checkout
Body: { planSlug: "mini" }
↓
Studeia createCheckoutSession() (lib/billing/create-checkout.ts):
1. resolveCustomerIdForProvider() — pega/cria stripeCustomerId
2. Detect currency via getCurrencyFromHeaders() (server-side, anti-fraud)
3. Stripe Checkout Session com line_items=[price_id correto]
4. Retorna { url } do Stripe Checkout
↓
Frontend redireciona: window.location.href = data.url
↓
Admin paga no Stripe Checkout
↓
Stripe envia webhook checkout.session.completed
↓
Studeia applyWebhookEvent():
1. Valida HMAC signature
2. stripe.subscriptions.retrieve(subscriptionId) — sempre re-fetch (nao confia em metadata)
3. Valida price_id contra allowlist
4. Cross-check tenantId entre session.metadata e subscription.metadata
5. Cria/atualiza TenantSubscription com currentPeriodEnd REAL
6. Promove Tenant.plan
7. Cria PaymentLog (idempotente via [provider, externalEventId])
Hardening (regras 129-139)
- Webhook retorna 5xx em falha: Stripe re-tenta 3 dias
- Ordering guard:
lastEventAt+lastEventIdem TenantSubscription. Eventos fora de ordem sao pulados com warning + PaymentLog - Idempotencia: unique [provider, externalEventId] em PaymentLog
- Schema Zod:
z.enum(PAID_PLAN_SLUGS)no body do checkout (anti-typo) - PII redact: PaymentLog.rawPayload passa por
redactPaymentPayload()(remove email/name/address/billing_details/cpfCnpj/card.last4 antes de persistir) - Past_due gate:
isAccessBlocked(status)em layouts (substituistatus === "suspended"). Cron/api/cron/billing-grace-expiretransiciona apos grace 7d
Portal Stripe self-manage
POST /api/institution/billing/portal retorna URL temporaria do Stripe portal:
- Admin troca plano (upgrade/downgrade)
- Atualiza cartao
- Ve invoices
- Cancela subscription
- Sem passar pelo suporte Studeia
Multi-currency
| Aspecto | BRL | USD |
|---|---|---|
| Detection | Default | Header x-vercel-ip-country / cf-ipcountry |
| Price IDs | STRIPE_PRICE_MINI, _GROWTH, _PRO_100 | STRIPE_PRICE_MINI_USD, _GROWTH_USD, _PRO_100_USD |
| Asaas fallback | Sim (PIX/boleto) | NAO (Asaas e Brasil only) |
| Webhook | Mesmo endpoint | Mesmo endpoint |