Billing & Brand Context API Routes
Rotas de billing (planos, assinaturas, pagamentos, webhook ASAAS) e brand context (perfil da marca para IA). Gateway de pagamento: ASAAS (Pix, Boleto, Cartão de Crédito).
GET /api/billing/plans
Seção intitulada “GET /api/billing/plans”Descrição: Lista todos os planos disponíveis com preços, limites e features.
Auth: Nenhuma (rota pública via withErrorHandling, sem getAuthContext)
Rate Limit: Nenhum
Audit: Nenhum
Request: N/A
Response:
- 200:
{ "data": [ { "id": "starter", "name": "Starter", "description": "string", "price": { "monthly": 9900, "yearly": 7900, "monthly_formatted": "R$ 99,00", "yearly_formatted": "R$ 79,00" }, "limits": { "contacts": 1000, "messagesPerMonth": 5000, "automations": 5, "teamMembers": 2, "whatsappNumbers": 1 }, "features": ["..."], "highlighted": false } ]}Notas: Dados carregados da constante PLANS em src/lib/billing. Preços em centavos, formatados com formatPrice().
POST /api/billing/subscribe
Seção intitulada “POST /api/billing/subscribe”Descrição: Cria uma nova assinatura. Cria customer no ASAAS, cria subscription, busca dados de pagamento (Pix QR code, Boleto URL, ou confirmação de cartão), e salva no banco.
Auth: Required (getAuthContext()) + role “owner” ou “admin”
Rate Limit: api preset (30/min)
Audit: create em subscription
Request:
- Body (Zod
subscribeSchema):
{ "plan": "string (ID do plano)", "billing_type": "PIX | BOLETO | CREDIT_CARD", "cycle": "MONTHLY | YEARLY", "billing_email": "string (opcional)", "cpf_cnpj": "string (obrigatório)", "credit_card": { "holder_name": "string", "number": "string", "expiry_month": "string", "expiry_year": "string", "ccv": "string" }, "credit_card_holder_info": { "name": "string", "email": "string", "cpf_cnpj": "string", "postal_code": "string", "address_number": "string", "phone": "string" }}credit_cardecredit_card_holder_info: obrigatórios apenas sebilling_type = "CREDIT_CARD"
Response:
- 201:
{ "data": { "subscription": { "...billing_subscriptions row" }, "asaas_subscription_id": "sub_xxx", "status": "ACTIVE | PENDING", "payment_data": { "type": "pix | boleto | credit_card", "payment_id": "pay_xxx" } }}- 401: Auth error
- 422: Plano inválido ou assinatura já ativa
- 429: Rate limit excedido
Notas:
- Verifica se já existe assinatura ativa/pending antes de criar
- Customer ASAAS criado com
externalReference = accountId - Status inicial: “active” para cartão, “pending” para Pix/Boleto
- Atualiza
accounts.planapós criação - Cria notificação in-app via
createNotification() - CPF/CNPJ sanitizado (remove tudo exceto digitos)
GET /api/billing/subscription
Seção intitulada “GET /api/billing/subscription”Descrição: Retorna a assinatura atual do account, o plano correspondente, e métricas de uso (contatos, mensagens do mês, automações, team members, números WhatsApp).
Auth: Required (getAuthContext())
Rate Limit: Nenhum
Audit: Nenhum
Request: N/A
Response:
- 200 (com assinatura):
{ "data": { "subscription": { "...billing_subscriptions fields", "value_formatted": "R$ 99,00" }, "plan": { "id": "starter", "name": "Starter", "limits": { "contacts": 1000, "..." }, "features": ["..."] }, "usage": { "contacts": 150, "messages": 2500, "automations": 3, "team": 2, "whatsapp": 1 } }}Notas: Busca assinatura com status diferente de “cancelled” e “inactive”. Mensagens contadas apenas as outbound do mês corrente. Usa 6 queries paralelas.
PUT /api/billing/subscription
Seção intitulada “PUT /api/billing/subscription”Descrição: Altera o plano de uma assinatura existente (upgrade ou downgrade). Valida limites de uso antes de downgrade.
Auth: Required (getAuthContext()) + role “owner” ou “admin”
Rate Limit: Nenhum
Audit: update em subscription
Request:
- Body (Zod
changePlanSchema):
{ "plan": "string (novo plan ID)", "cycle": "MONTHLY | YEARLY (opcional, mantem o atual se não informado)"}Response:
- 200:
{ "data": { "subscription": { "...updated row" }, "previous_plan": "starter", "new_plan": "professional" }}- 401: Auth error
- 404: Nenhuma assinatura ativa
- 422: Plano inválido, já está neste plano, ou downgrade bloqueado por uso
Notas:
- Valida que o uso atual cabe nos limites do novo plano (exceto messagesPerMonth)
- Se existe
asaas_subscription_id, atualiza valor e ciclo no ASAAS - Atualiza
billing_subscriptionseaccounts.planvia admin client - Cria notificação in-app com plano anterior e novo
POST /api/billing/cancel
Seção intitulada “POST /api/billing/cancel”Descrição: Cancela a assinatura atual. Cancela no ASAAS e marca como “cancelled” no banco.
Auth: Required (getAuthContext()) + role “owner” ou “admin”
Rate Limit: api preset (30/min)
Audit: delete em subscription
Request: N/A
Response:
- 200:
{ "data": { "cancelled": true, "effective_until": "2026-03-20T..." }}- 401: Auth error
- 404: Nenhuma assinatura ativa
- 429: Rate limit excedido
Notas:
effective_untilé ocurrent_period_endda assinatura (acesso permanece até o fim do período)- Cancela subscription no ASAAS se
asaas_subscription_idexiste - Seta
status = "cancelled"ecancelled_at = now() - Cria notificação in-app tipo “warning”
GET /api/billing/history
Seção intitulada “GET /api/billing/history”Descrição: Lista histórico de pagamentos do account com paginação offset-based.
Auth: Required (getAuthContext())
Rate Limit: Nenhum
Audit: Nenhum
Request:
- Query params:
page(number, default: 1) - páginalimit(number) - itens por página
Response:
- 200:
{ "data": { "items": [ { "...billing_history fields", "value_formatted": "R$ 99,00" } ], "total": 12, "page": 1, "limit": 20 }}Notas: Ordenado por created_at desc. Valor formatado com formatPrice().
GET /api/billing/payment-status
Seção intitulada “GET /api/billing/payment-status”Descrição: Consulta o status de um pagamento específico no ASAAS.
Auth: Required (getAuthContext())
Rate Limit: Nenhum
Audit: Nenhum
Request:
- Query params:
payment_id(string, obrigatório) - ID do pagamento ASAAS
Response:
- 200:
{ "data": { "status": "CONFIRMED | PENDING | OVERDUE | ...", "billing_type": "PIX | BOLETO | CREDIT_CARD", "invoice_url": "https://..." }}- 422:
payment_idnão informado
Notas: Chama diretamente a API do ASAAS via getAsaasClient().getPayment().
POST /api/billing/webhook
Seção intitulada “POST /api/billing/webhook”Descrição: Webhook receiver para eventos do ASAAS. Processa pagamentos confirmados, pagamentos vencidos, e subscriptions inativadas.
Auth: ASAAS webhook token (header asaas-access-token, comparado com ASAAS_WEBHOOK_TOKEN env var)
Rate Limit: webhook preset (100/min, por IP)
Audit: Nenhum
Request:
- Headers:
asaas-access-token: string - Body:
{ "event": "PAYMENT_CONFIRMED | PAYMENT_RECEIVED | PAYMENT_OVERDUE | SUBSCRIPTION_INACTIVATED", "payment": { "id": "pay_xxx", "subscription": "sub_xxx", "billingType": "PIX", "value": 99.00, "invoiceUrl": "https://...", "bankSlipUrl": "https://...", "dueDate": "2026-02-25", "status": "CONFIRMED" }}Response:
- 200:
{ "received": true }(sempre, mesmo com erros internos de processamento) - 401: Token ausente ou inválido
- 500: ASAAS_WEBHOOK_TOKEN não configurado
Notas:
dynamic = "force-dynamic"(Vercel)- Token comparado com
timingSafeEqualpara prevenir timing attacks - NAO usa
withErrorHandling— error handling manual - Sempre retorna 200 após validação do token (mesmo se processamento falhar)
- Eventos processados:
PAYMENT_CONFIRMED/PAYMENT_RECEIVED: ativa assinatura, atualiza accounts.plan, upsert em billing_history, notificação “success”PAYMENT_OVERDUE: marca assinatura como “past_due”, upsert em billing_history, notificação “warning”SUBSCRIPTION_INACTIVATED: marca assinatura como “inactive”
- Usa
createAdminClient()para bypass RLS
GET /api/brand-context
Seção intitulada “GET /api/brand-context”Descrição: Retorna o brand context do account (perfil da marca: nome, descrição, tom, público, produtos, etc.).
Auth: Required (getAuthContext())
Rate Limit: Nenhum
Audit: Nenhum
Request: N/A
Response:
- 200:
{ "data": BrandContext | null }
PUT /api/brand-context
Seção intitulada “PUT /api/brand-context”Descrição: Cria ou atualiza o brand context do account (upsert).
Auth: Required (getAuthContext())
Rate Limit: Nenhum
Audit: update em brand_context
Request:
- Body (Zod
brandContextSchema):
{ "company_name": "string | null", "description": "string | null", "products_services": "string | null", "tone_of_voice": "string | null", "target_audience": "string | null", "brand_values": "string | null", "website_url": "string | null"}Response:
- 200:
{ "data": BrandContext } - 401: Auth error
- 422: Validation error
POST /api/brand-context/scrape
Seção intitulada “POST /api/brand-context/scrape”Descrição: Faz scraping de um website e usa IA (Claude) para analisar e extrair informações da marca. Salva resultado como brand context do account.
Auth: Required (getAuthContext())
Rate Limit: api preset (30/min)
Audit: Nenhum
Request:
- Body (Zod
scrapeSchema):
{ "url": "string (URL valida, obrigatório)"}Response:
- 200:
{ "data": BrandContext } - 401: Auth error
- 422: URL inválida, protocolo não permitido, hostname não resolve, IP privado, resposta > 1MB
- 429: Rate limit excedido
Notas:
- Proteção SSRF: resolve hostname via DNS e válida contra ranges de IP privados (10.x, 172.16.x, 192.168.x, 127.x, 169.254.x, 0.x, ::1)
- Apenas protocolos
http:ehttps:permitidos - Timeout de 10 segundos para o fetch
- Limite de resposta: 1MB (verificado via Content-Length header É tamanho real do body)
- User-Agent:
Mozilla/5.0 (compatible; x17-bot/1.0) - O HTML é enviado ao Claude via
analyzeBrand()(VPS proxy) que extrai: company_name, description, products_services, tone_of_voice, target_audience, brand_values - Resultado salvo via
brandContexts.upsert()comraw_scrape_dataelast_scraped_at