Pular para o conteúdo

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).


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().


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_card e credit_card_holder_info: obrigatórios apenas se billing_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.plan após criação
  • Cria notificação in-app via createNotification()
  • CPF/CNPJ sanitizado (remove tudo exceto digitos)

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.


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_subscriptions e accounts.plan via admin client
  • Cria notificação in-app com plano anterior e novo

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 é o current_period_end da assinatura (acesso permanece até o fim do período)
  • Cancela subscription no ASAAS se asaas_subscription_id existe
  • Seta status = "cancelled" e cancelled_at = now()
  • Cria notificação in-app tipo “warning”

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ágina
    • limit (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().


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_id não informado

Notas: Chama diretamente a API do ASAAS via getAsaasClient().getPayment().


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 timingSafeEqual para 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

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 }

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

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: e https: 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() com raw_scrape_data e last_scraped_at