Setup
Asaas Dashboard
- https://asaas.com (or sandbox: https://sandbox.asaas.com)
- Settings > Integrations > API > copy Access Token
- Settings > Webhooks > URL:
https://[tenant].studeia.com/api/webhooks/asaas?token=<TOKEN> - Events: PAYMENT_RECEIVED, PAYMENT_OVERDUE, PAYMENT_REFUNDED, SUBSCRIPTION_CREATED/UPDATED/DELETED
- Custom token (cryptographically strong random string)
Env vars
ASAAS_API_KEY=$aact_...
ASAAS_WEBHOOK_TOKEN=...generated-in-step-5...
ASAAS_SANDBOX=true # optional
PIX flow
Admin clicks "Pay with PIX" → POST /api/institution/billing/checkout { provider: "asaas" } → Studeia AsaasBillingProvider.createCheckout() (resolves asaasCustomerId, POST /subscriptions with billingType=PIX) → Asaas returns QR Code + PIX copy-paste payload → Studeia returns Asaas hosted page URL → admin scans QR, pays → PIX confirms in ~30s → webhook PAYMENT_RECEIVED → applyWebhookEvent → promotes Tenant.plan.
Hardening (rules 138, 139)
- Fail-closed: missing ASAAS_WEBHOOK_TOKEN env = 503
asaas_not_configured - Token validation: query param ?token=<...> compared with env (timing-safe)
- 5xx on failure: auto-retries
- PaymentLog idempotent
- PII redact: rawPayload via redactPaymentPayload() — removes cpfCnpj, email, name, address
Customer split
TenantSubscription separate fields: stripeCustomerId, asaasCustomerId, externalCustomerId (legacy fallback).
When Asaas vs Stripe
| Scenario | Recommended |
|---|---|
| Brazilian client with PIX | Asaas |
| Brazilian client with boleto | Asaas |
| Brazilian client with card | Asaas (cheaper) or Stripe |
| International client | Stripe USD (Asaas blocked) |
Fees compared
| Method | Asaas | Stripe |
|---|---|---|
| PIX | 1.99% | NOT offered |
| Boleto | R$3.49 fixed | NOT offered |
| Credit card | 4.99% + R$0.49 | 3.99% + R$0.59 (USD) |
For typical B2B Brazilian volume: Asaas is ~20-40% cheaper in total fees.