openapi: 3.0.3 info: title: AraraHQ API description: | AraraHQ é um CPaaS brasileiro, "Resend pra WhatsApp". Esta API expõe envio de mensagens WhatsApp, gestão de templates, campanhas, contatos, conversas, wallet, números e mais. Esta é a **V2** da documentação. O escopo é exclusivamente **WhatsApp** (SMS e canais legados foram removidos). Os paths reais do backend ficam em `/v1/*`, `/auth/*`, `/dashboard/*` e afins — "V2" se refere à versão da doc, não a um prefixo de URL. ## Autenticação Dois esquemas convivem: - **`bearerAuth`** — API key emitida pelo dashboard. Header `Authorization: Bearer ara_live_...`. Usado por todos os endpoints `/v1/*` e `/dashboard/*`. - **`firebaseAuth`** — Firebase ID token. Usado em `/auth/*`, `/users/*` e `/v1/profile/*` (operações que pertencem ao usuário, não à organização). As chaves são sempre LIVE (`ara_live_...`). Não há chave de sandbox nem modo de teste — mensagens entregam de verdade e créditos são debitados. ## Rate limiting Requests acima do limite recebem `429 Too Many Requests` com header `Retry-After`. Honre o header antes de retentar. ## Formato de erro Todo erro segue o formato canônico: ```json { "error": { "code": "ERROR_CODE", "message": "Descrição legível", "details": {} } } ``` version: 2.0.0 contact: name: AraraHQ Support url: https://ararahq.com email: support@ararahq.com servers: - url: https://api.ararahq.com description: Produção security: - bearerAuth: [] tags: - name: auth description: Sessão, identidade do usuário autenticado e listagem de organizações. - name: messages description: Envio e consulta de mensagens WhatsApp. - name: templates description: Criação, listagem, status e analytics de templates WhatsApp. - name: campaigns description: Campanhas em massa, estimativa de custo, cancelamento e A/B testing. - name: contacts description: Listagem, importação em lote e LGPD-friendly lookup de contatos. - name: conversations description: Inbox de conversas, mensagens, reply em janela de 24h e status da janela. - name: api-keys description: 'Gestão de chaves de API: criar, listar, rotacionar, revogar, stats e events.' - name: webhook-config description: Configuração do webhook outbound da organização. - name: webhooks description: Webhook de carrinho abandonado (recurso histórico). - name: wallet description: Cálculo de recarga, auto-recharge e extrato de transações. - name: payments description: Checkout Stripe e PIX via AbacatePay. - name: numbers description: Números dedicados, requests de novo número e warming. - name: business-profile description: Perfil do negócio sincronizado com Meta/Twilio. - name: profile description: Perfil pessoal, MFA, mudança de email e LGPD. - name: recovery description: Endpoint de Recovery (carrinho abandonado), eventos e ingests. - name: pricing description: Preços por categoria de template e país. - name: organization description: Plano e dados da organização autenticada. - name: dashboard description: Métricas agregadas para o dashboard. - name: health description: Health check. paths: # ---------------------- AUTH ---------------------- /auth: post: tags: [auth] operationId: firebaseLogin summary: Troca um Firebase ID token por sessão Arara description: | Recebe um Firebase ID token e retorna o `UserResponseDTO`, um token de sessão e (eventualmente) uma API key inicial. Endpoint público — não requer Authorization. IP do cliente é extraído de `X-Forwarded-For` / `X-Real-IP` quando vindo via CDN. security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/FirebaseLoginRequest' responses: '200': description: Login OK content: application/json: schema: $ref: '#/components/schemas/AuthResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/TooManyRequests' } /auth/me: get: tags: [auth] operationId: getCurrentUser summary: Retorna o usuário autenticado security: - firebaseAuth: [] - bearerAuth: [] responses: '200': description: Usuário autenticado content: application/json: schema: $ref: '#/components/schemas/UserResponseDTO' '401': { $ref: '#/components/responses/Unauthorized' } /auth/me/organizations: get: tags: [auth] operationId: getMyOrganizations summary: Lista organizações do usuário autenticado description: | Lê membership do usuário em todas as orgs. A org primária está incluída pelo backfill V117 — se por algum motivo faltar, é adicionada no topo. security: - firebaseAuth: [] - bearerAuth: [] responses: '200': description: Lista de organizações content: application/json: schema: $ref: '#/components/schemas/OrganizationListResponse' '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- MESSAGES ---------------------- /v1/messages: post: tags: [messages] operationId: sendMessage summary: Envia mensagem WhatsApp (template, texto, interativa, localização ou reação) description: | Aceita exatamente **um** tipo de payload por request: `templateName`, `body`, `interactive`, `location` ou `reaction`. Retorna `202 Accepted` com o ID interno; o status real chega via webhook. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SendMessageRequest' responses: '202': description: Mensagem aceita pra envio content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '422': { $ref: '#/components/responses/UnprocessableEntity' } '429': { $ref: '#/components/responses/TooManyRequests' } /v1/messages/batch: post: tags: [messages] operationId: sendBatchMessages summary: Envia múltiplas mensagens com o mesmo template description: Limite de **1000 mensagens** por lote. Faz batch insert e enfileira em lote. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BatchMessageRequest' responses: '202': description: Lote aceito content: application/json: schema: $ref: '#/components/schemas/BatchMessageResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '422': { $ref: '#/components/responses/UnprocessableEntity' } '429': { $ref: '#/components/responses/TooManyRequests' } /v1/messages/{internalId}: get: tags: [messages] operationId: getMessage summary: Busca uma mensagem pelo ID interno parameters: - $ref: '#/components/parameters/MessageInternalId' responses: '200': description: Mensagem encontrada content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } # ---------------------- TEMPLATES ---------------------- /v1/templates: post: tags: [templates] operationId: createTemplate summary: Cria e submete um template à Twilio/Meta para aprovação description: | Requer conta ACTIVE (live mode). Plano FREE limitado a 3 templates. Submete pra Twilio; status inicial fica `PENDING`. Use `/v1/templates/{id}/status` pra checar aprovação. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateTemplateRequest' responses: '201': description: Template criado e submetido content: application/json: schema: $ref: '#/components/schemas/TemplateResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '422': { $ref: '#/components/responses/UnprocessableEntity' } get: tags: [templates] operationId: listTemplates summary: Lista templates da organização parameters: - in: query name: name schema: { type: string } required: false description: Filtra por nome exato. - in: query name: status schema: type: string enum: [PENDING, APPROVED, REJECTED, PAUSED, DISABLED] required: false responses: '200': description: Lista de templates content: application/json: schema: type: array items: $ref: '#/components/schemas/TemplateResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /v1/templates/{id}: get: tags: [templates] operationId: getTemplate summary: Busca um template específico parameters: - $ref: '#/components/parameters/TemplateId' responses: '200': description: Template content: application/json: schema: $ref: '#/components/schemas/TemplateResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [templates] operationId: deleteTemplate summary: Remove um template parameters: - $ref: '#/components/parameters/TemplateId' responses: '204': description: Removido '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /v1/templates/{id}/status: get: tags: [templates] operationId: getTemplateStatus summary: Consulta status atualizado de aprovação na Twilio/Meta description: | Bate na Twilio (source of truth), persiste o status localmente e retorna. Em caso de falha do provider, devolve o status já no banco. parameters: - $ref: '#/components/parameters/TemplateId' responses: '200': description: Status content: application/json: schema: $ref: '#/components/schemas/TemplateStatusResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /v1/templates/{id}/analytics: get: tags: [templates] operationId: getTemplateAnalytics summary: Analytics agregadas de um template description: Conta mensagens enviadas, entregues, lidas e falhadas via GROUP BY no banco. parameters: - $ref: '#/components/parameters/TemplateId' - in: query name: period schema: type: string enum: [7d, 30d, 90d] default: 30d required: false responses: '200': description: Analytics content: application/json: schema: $ref: '#/components/schemas/TemplateAnalyticsResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } # ---------------------- CAMPAIGNS ---------------------- /v1/campaigns: post: tags: [campaigns] operationId: createCampaign summary: Cria uma campanha em massa description: | Requer plano PRO+ e número dedicado atrelado à organização. Header `Idempotency-Key` é obrigatório — mesmo header em retries retorna a mesma campanha. parameters: - in: header name: Idempotency-Key required: true schema: { type: string } description: Identificador único do retry (UUID recomendado). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CampaignRequest' responses: '200': description: Campanha criada content: application/json: schema: $ref: '#/components/schemas/CampaignResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } get: tags: [campaigns] operationId: listCampaigns summary: Lista campanhas da organização parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/SizeParam' - in: query name: status schema: type: string enum: [PENDING, RUNNING, PAUSED, COMPLETED, CANCELED, FAILED] required: false responses: '200': description: Lista paginada content: application/json: schema: $ref: '#/components/schemas/CampaignListResponse' '401': { $ref: '#/components/responses/Unauthorized' } /v1/campaigns/estimate: get: tags: [campaigns] operationId: estimateCampaign summary: Estima custo de uma campanha antes de criar parameters: - in: query name: templateName required: true schema: { type: string } - in: query name: count required: true schema: type: integer minimum: 1 description: Número de destinatários. responses: '200': description: Estimativa content: application/json: schema: $ref: '#/components/schemas/CampaignEstimateResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /v1/campaigns/{id}: get: tags: [campaigns] operationId: getCampaign summary: Detalhe de uma campanha com métricas parameters: - $ref: '#/components/parameters/CampaignId' responses: '200': description: Detalhe content: application/json: schema: $ref: '#/components/schemas/CampaignDetailResponse' '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /v1/campaigns/{id}/cancel: post: tags: [campaigns] operationId: cancelCampaign summary: Cancela uma campanha em andamento parameters: - $ref: '#/components/parameters/CampaignId' responses: '204': description: Cancelada '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /v1/campaigns/{campaignId}/ab-test: post: tags: [campaigns] operationId: createCampaignAbTest summary: Cria um A/B test para uma campanha parameters: - $ref: '#/components/parameters/CampaignIdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAbTestRequest' responses: '201': description: A/B test criado content: application/json: schema: $ref: '#/components/schemas/AbTestResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } get: tags: [campaigns] operationId: getCampaignAbTest summary: Consulta A/B test de uma campanha parameters: - $ref: '#/components/parameters/CampaignIdPath' responses: '200': description: A/B test content: application/json: schema: $ref: '#/components/schemas/AbTestResponse' '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /v1/campaigns/{campaignId}/ab-test/winner: post: tags: [campaigns] operationId: forceCampaignAbTestWinner summary: Força um vencedor manualmente no A/B test parameters: - $ref: '#/components/parameters/CampaignIdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ForceWinnerRequest' responses: '200': description: Vencedor aplicado content: application/json: schema: $ref: '#/components/schemas/AbTestResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } # ---------------------- CONTACTS ---------------------- /v1/contacts: get: tags: [contacts] operationId: listContacts summary: Lista contatos da organização parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/SizeParam' - in: query name: q schema: { type: string } required: false description: Busca por nome, telefone ou email. responses: '200': description: Lista content: application/json: schema: $ref: '#/components/schemas/ContactsListResponse' '401': { $ref: '#/components/responses/Unauthorized' } /v1/contacts/batch: post: tags: [contacts] operationId: importContactsBatch summary: Importa contatos em lote requestBody: required: true content: application/json: schema: type: array items: $ref: '#/components/schemas/ContactRequest' responses: '200': description: Resultado do import content: application/json: schema: $ref: '#/components/schemas/ContactsBatchResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /v1/contacts/stats: get: tags: [contacts] operationId: getContactsStats summary: Stats agregadas de contatos da organização responses: '200': description: Mapa de stats content: application/json: schema: type: object additionalProperties: type: integer format: int64 '401': { $ref: '#/components/responses/Unauthorized' } /v1/contacts/{phone}: get: tags: [contacts] operationId: getContactByPhone summary: Busca contato por telefone E.164 parameters: - in: path name: phone required: true schema: type: string pattern: '^\+[1-9]\d{6,14}$' description: Telefone E.164 (ex. `+5511999998888`). responses: '200': description: Contato content: application/json: schema: $ref: '#/components/schemas/ContactResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } # ---------------------- API KEYS ---------------------- /v1/api-keys: post: tags: [api-keys] operationId: createApiKey summary: Cria uma nova API key description: | Retorna a chave em texto plano **uma única vez**. Modo LIVE exige conta ACTIVE. `ipAllowlist` é feature do plano Business. parameters: - in: query name: mode schema: type: string enum: [LIVE] default: LIVE required: false requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/CreateApiKeyRequest' responses: '201': description: API key criada (chave plain text só aqui) content: application/json: schema: $ref: '#/components/schemas/GeneratedApiKey' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } get: tags: [api-keys] operationId: listApiKeys summary: Lista API keys ativas do usuário responses: '200': description: Lista (somente metadados, nunca o segredo) content: application/json: schema: type: array items: $ref: '#/components/schemas/ApiKeyListItem' '401': { $ref: '#/components/responses/Unauthorized' } /v1/api-keys/{id}: patch: tags: [api-keys] operationId: updateApiKey summary: Atualiza nome, escopo, expiração ou IP allowlist parameters: - $ref: '#/components/parameters/ApiKeyId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateApiKeyRequest' responses: '204': description: Atualizado '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [api-keys] operationId: revokeApiKey summary: Revoga uma API key parameters: - $ref: '#/components/parameters/ApiKeyId' responses: '204': description: Revogada '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /v1/api-keys/{id}/rotate: post: tags: [api-keys] operationId: rotateApiKey summary: Rotaciona uma API key (gera nova, marca antiga como rotacionada) parameters: - $ref: '#/components/parameters/ApiKeyId' responses: '200': description: Nova chave em texto plano content: application/json: schema: $ref: '#/components/schemas/GeneratedApiKey' '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /v1/api-keys/{id}/stats: get: tags: [api-keys] operationId: getApiKeyStats summary: Stats de uso da chave (7d / 30d) parameters: - $ref: '#/components/parameters/ApiKeyId' responses: '200': description: Stats content: application/json: schema: $ref: '#/components/schemas/ApiKeyStatsResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /v1/api-keys/{id}/events: get: tags: [api-keys] operationId: getApiKeyEvents summary: Eventos de auditoria da chave (created, rotated, revoked, etc.) parameters: - $ref: '#/components/parameters/ApiKeyId' responses: '200': description: Lista de eventos content: application/json: schema: type: array items: $ref: '#/components/schemas/ApiKeyEvent' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } # ---------------------- CONVERSATIONS ---------------------- /v1/conversations: get: tags: [conversations] operationId: listConversations summary: Lista conversas (inbox) description: Requer plano PRO+ e número dedicado. parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/SizeParam' - in: query name: status schema: { type: string } required: false - in: query name: leadStatus schema: { type: string } required: false responses: '200': description: Lista paginada de conversas content: application/json: schema: $ref: '#/components/schemas/ConversationListResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /v1/conversations/lead-stats: get: tags: [conversations] operationId: getConversationLeadStats summary: Contagem de leads por status responses: '200': description: Stats content: application/json: schema: type: object properties: byStatus: type: object additionalProperties: type: integer format: int64 '401': { $ref: '#/components/responses/Unauthorized' } /v1/conversations/{conversationId}/messages: get: tags: [conversations] operationId: listConversationMessages summary: Lista mensagens de uma conversa parameters: - in: path name: conversationId required: true schema: type: string format: uuid - $ref: '#/components/parameters/PageParam' - in: query name: size schema: type: integer default: 50 required: false responses: '200': description: Lista paginada de mensagens content: application/json: schema: $ref: '#/components/schemas/ConversationMessagesResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /v1/conversations/reply: post: tags: [conversations] operationId: replyToConversation summary: Envia mensagem de texto livre dentro da janela 24h description: Falha com 400 se a janela já expirou — nesse caso envie um template pra reabrir. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ConversationReplyRequest' responses: '200': description: Mensagem enviada content: application/json: schema: $ref: '#/components/schemas/ConversationReplyResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /v1/conversations/window-status: post: tags: [conversations] operationId: getWindowStatus summary: Checa status da janela 24h para múltiplos telefones description: 'Limite de 200 telefones por request — extras são truncados (`truncated: true`).' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WindowStatusRequest' responses: '200': description: Status por telefone content: application/json: schema: $ref: '#/components/schemas/WindowStatusResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- WEBHOOK CONFIG ---------------------- /organizations/me/webhook: get: tags: [webhook-config] operationId: getWebhookConfig summary: Configuração do webhook outbound da organização responses: '200': description: Config content: application/json: schema: $ref: '#/components/schemas/WebhookConfigResponse' '401': { $ref: '#/components/responses/Unauthorized' } patch: tags: [webhook-config] operationId: updateWebhookConfig summary: Atualiza URL e/ou secret do webhook outbound requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateWebhookRequest' responses: '200': description: Atualizado content: application/json: schema: type: object properties: message: { type: string } url: { type: string } secret: { type: string } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '500': { $ref: '#/components/responses/InternalServerError' } /organizations/me/webhook/reenable: post: tags: [webhook-config] operationId: reenableWebhook summary: Reativa um webhook desabilitado por falhas consecutivas responses: '200': description: Reativado content: application/json: schema: type: object properties: message: { type: string } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- WEBHOOKS (inbound do cliente) ---------------------- /webhook/cart: post: tags: [webhooks] operationId: cartAbandonedWebhook summary: Recebe webhook de carrinho abandonado (legado pré-Recovery) description: | Endpoint legado de carrinho abandonado. Para novas integrações use o fluxo de **Recovery** (`/v1/recovery/*`). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CartAbandonedWebhookRequest' responses: '202': description: Aceito pra processamento content: application/json: schema: $ref: '#/components/schemas/CartAbandonedWebhookResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- WALLET ---------------------- /v1/wallet/calculate: get: tags: [wallet] operationId: calculateRecharge summary: Calcula quantas mensagens cada categoria compra para um valor parameters: - in: query name: amountCents required: true schema: type: integer minimum: 10000 maximum: 5000000 responses: '200': description: Estimativa content: application/json: schema: $ref: '#/components/schemas/RechargeEstimate' '401': { $ref: '#/components/responses/Unauthorized' } /v1/wallet/auto-recharge: get: tags: [wallet] operationId: getAutoRecharge summary: Lê configuração de auto-recharge da wallet responses: '200': description: Config content: application/json: schema: $ref: '#/components/schemas/AutoRechargeSettings' '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } patch: tags: [wallet] operationId: updateAutoRecharge summary: Atualiza threshold, amount e enabled description: Ativar exige cartão cadastrado (assinatura Stripe). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateAutoRechargeRequest' responses: '200': description: Config atualizada content: application/json: schema: $ref: '#/components/schemas/AutoRechargeSettings' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /v1/wallet/transactions: get: tags: [wallet] operationId: listWalletTransactions summary: Extrato da wallet parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/SizeParam' responses: '200': description: Página de transações content: application/json: schema: $ref: '#/components/schemas/WalletTransactionPage' '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- PAYMENTS ---------------------- /payments/pix: post: tags: [payments] operationId: createPixPayment summary: Cria cobrança PIX para recarga via AbacatePay requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreatePaymentRequest' responses: '200': description: PIX criado content: application/json: schema: $ref: '#/components/schemas/AbacatePayPixResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /payments/pix/{id}/status: get: tags: [payments] operationId: getPixStatus summary: Consulta status de uma cobrança PIX parameters: - in: path name: id required: true schema: { type: string } responses: '200': description: Status content: application/json: schema: $ref: '#/components/schemas/AbacatePayStatusResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /v1/payments/create-checkout-session: post: tags: [payments] operationId: createStripeCheckoutSession summary: Cria sessão de Stripe Checkout (assinatura ou recarga one-time) description: | - `uiMode = "hosted"` retorna `url`; redirecione o cliente. - `uiMode = "embedded"` retorna `clientSecret`; renderize com ``. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PaymentRequestDTO' responses: '200': description: Sessão criada content: application/json: schema: $ref: '#/components/schemas/CheckoutSessionResponse' '401': { $ref: '#/components/responses/Unauthorized' } '500': { $ref: '#/components/responses/InternalServerError' } # ---------------------- NUMBERS ---------------------- /v1/organizations/me/numbers: get: tags: [numbers] operationId: listNumbers summary: Lista números da organização com health e slot do plano responses: '200': description: Números + slot content: application/json: schema: $ref: '#/components/schemas/NumbersResponse' '401': { $ref: '#/components/responses/Unauthorized' } /v1/organizations/me/numbers/{id}: patch: tags: [numbers] operationId: updateNumber summary: Atualiza nome, alias, description ou flag default parameters: - $ref: '#/components/parameters/NumberId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateNumberRequest' responses: '200': description: Atualizado content: application/json: schema: type: object properties: status: { type: string, example: ok } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /v1/organizations/me/numbers/request: post: tags: [numbers] operationId: requestNewNumber summary: Solicita um novo número dedicado (provisionamento manual) requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RequestNumberRequest' responses: '202': description: Pedido recebido content: application/json: schema: $ref: '#/components/schemas/RequestNumberResponse' '401': { $ref: '#/components/responses/Unauthorized' } '409': { $ref: '#/components/responses/Conflict' } /v1/organizations/me/numbers/requests: get: tags: [numbers] operationId: listNumberRequests summary: Lista solicitações de novos números (pending + resolvidas) responses: '200': description: Lista content: application/json: schema: type: array items: $ref: '#/components/schemas/NumberRequestItem' '401': { $ref: '#/components/responses/Unauthorized' } /v1/organizations/me/numbers/{id}/sync: post: tags: [numbers] operationId: syncNumberFromProvider summary: Força sync com Twilio (quality, tier, status, verifiedAt) parameters: - $ref: '#/components/parameters/NumberId' responses: '200': description: Resultado do sync content: application/json: schema: $ref: '#/components/schemas/NumberSyncResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /v1/organizations/me/numbers/{id}/warming: get: tags: [numbers] operationId: getNumberWarming summary: Recomendação de warming pra um número parameters: - $ref: '#/components/parameters/NumberId' responses: '200': description: Recomendação content: application/json: schema: $ref: '#/components/schemas/NumberWarmingResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } # ---------------------- BUSINESS PROFILE ---------------------- /v1/organizations/me/business-profile: get: tags: [business-profile] operationId: getBusinessProfile summary: Lê perfil do negócio responses: '200': description: Perfil content: application/json: schema: $ref: '#/components/schemas/BusinessProfileResponse' '401': { $ref: '#/components/responses/Unauthorized' } patch: tags: [business-profile] operationId: updateBusinessProfile summary: Atualiza campos do perfil; marca metaSyncStatus=PENDING requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BusinessProfilePatch' responses: '200': description: Perfil atualizado content: application/json: schema: $ref: '#/components/schemas/BusinessProfileResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /v1/organizations/me/business-profile/sync: post: tags: [business-profile] operationId: syncBusinessProfile summary: Envia perfil pra Twilio/Meta sincronizar responses: '200': description: Sync executado content: application/json: schema: $ref: '#/components/schemas/BusinessProfileSyncResponse' '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } # ---------------------- PROFILE / LGPD ---------------------- /v1/profile/me: get: tags: [profile] operationId: getProfile summary: Perfil do usuário autenticado security: - firebaseAuth: [] - bearerAuth: [] responses: '200': description: Perfil content: application/json: schema: $ref: '#/components/schemas/ProfileResponse' '401': { $ref: '#/components/responses/Unauthorized' } /v1/profile: patch: tags: [profile] operationId: patchProfile summary: Atualiza campos do perfil pessoal security: - firebaseAuth: [] - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ProfilePatch' responses: '204': description: Atualizado '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /v1/profile/export: get: tags: [profile] operationId: exportProfileData summary: Export LGPD dos dados do usuário security: - firebaseAuth: [] - bearerAuth: [] responses: '200': description: Dump dos dados content: application/json: schema: type: object additionalProperties: true '401': { $ref: '#/components/responses/Unauthorized' } /v1/profile/delete-account: post: tags: [profile] operationId: deleteAccount summary: Apaga conta (LGPD); confirmação textual obrigatória description: | `confirmation` deve ser exatamente `EXCLUIR `. security: - firebaseAuth: [] - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DeleteAccountRequest' responses: '204': description: Conta apagada '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- RECOVERY ---------------------- /v1/recovery/endpoint: get: tags: [recovery] operationId: getRecoveryEndpoint summary: Lê (ou cria) o endpoint de Recovery da organização responses: '200': description: Endpoint content: application/json: schema: $ref: '#/components/schemas/RecoveryEndpointResponse' '401': { $ref: '#/components/responses/Unauthorized' } patch: tags: [recovery] operationId: patchRecoveryEndpoint summary: Ativa ou desativa o endpoint de Recovery requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RecoveryEndpointPatchRequest' responses: '200': description: Endpoint atualizado content: application/json: schema: $ref: '#/components/schemas/RecoveryEndpointResponse' '401': { $ref: '#/components/responses/Unauthorized' } /v1/recovery/endpoint/regenerate: post: tags: [recovery] operationId: regenerateRecoveryEndpoint summary: Gera novo HMAC secret pro endpoint responses: '200': description: Endpoint regenerado content: application/json: schema: $ref: '#/components/schemas/RecoveryEndpointResponse' '401': { $ref: '#/components/responses/Unauthorized' } /v1/recovery/events: get: tags: [recovery] operationId: listRecoveryEvents summary: Lista configurações de eventos do Recovery responses: '200': description: Lista content: application/json: schema: $ref: '#/components/schemas/RecoveryEventsListResponse' '401': { $ref: '#/components/responses/Unauthorized' } /v1/recovery/events/{eventType}: put: tags: [recovery] operationId: upsertRecoveryEventConfig summary: Cria ou atualiza configuração de um event type parameters: - in: path name: eventType required: true schema: { type: string } description: 'Ex: `cart.abandoned`, `pix.created`, `payment.failed`.' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RecoveryEventConfigPatchRequest' responses: '200': description: Config content: application/json: schema: $ref: '#/components/schemas/RecoveryEventConfigResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /v1/recovery/events/{eventType}/test: post: tags: [recovery] operationId: testRecoveryEvent summary: Envia um payload de teste pro evento (dispara mensagem real ao usuário) parameters: - in: path name: eventType required: true schema: { type: string } responses: '200': description: Resultado da ingestão de teste content: application/json: schema: $ref: '#/components/schemas/RecoveryIngestResult' '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- PRICING ---------------------- /v1/pricing/current: get: tags: [pricing] operationId: getCurrentPricing summary: Preços vigentes da organização (por categoria e por país) parameters: - in: query name: countryPrefix schema: { type: string } required: false description: Filtra preços de mensagens por prefixo de país (ex `55`). responses: '200': description: Preços content: application/json: schema: $ref: '#/components/schemas/PricingResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- ORGANIZATION ---------------------- /v1/organizations/me/plan: get: tags: [organization] operationId: getOrganizationPlan summary: Plano atual da organização e features responses: '200': description: Plano content: application/json: schema: $ref: '#/components/schemas/PlanInfoResponse' '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- DASHBOARD ---------------------- /dashboard/messages: get: tags: [dashboard] operationId: getDashboardMessages summary: Lista mensagens da organização (filtrada por modo) parameters: - $ref: '#/components/parameters/ModeParam' - $ref: '#/components/parameters/PageParam' - in: query name: size schema: type: integer default: 50 required: false responses: '200': description: Mensagens paginadas content: application/json: schema: $ref: '#/components/schemas/DashboardMessagesResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /dashboard/metrics: get: tags: [dashboard] operationId: getDashboardMetrics summary: Métricas agregadas (count por status, custo, delivery rate) parameters: - $ref: '#/components/parameters/ModeParam' responses: '200': description: Métricas content: application/json: schema: $ref: '#/components/schemas/DashboardMetricsResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /dashboard/wallet/balance: get: tags: [dashboard] operationId: getWalletBalance summary: Saldo da wallet por modo parameters: - $ref: '#/components/parameters/ModeParam' responses: '200': description: Saldo content: application/json: schema: $ref: '#/components/schemas/WalletBalanceResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } # ---------------------- HEALTH ---------------------- /health: get: tags: [health] operationId: health summary: Health check do serviço security: [] responses: '200': description: UP content: application/json: schema: $ref: '#/components/schemas/HealthResponse' components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: ara_live_xxx description: API key emitida pelo dashboard (`ara_live_*`, LIVE-only). firebaseAuth: type: http scheme: bearer bearerFormat: Firebase ID Token description: Firebase ID token para endpoints atrelados ao usuário (`/auth/*`, `/v1/profile/*`). parameters: PageParam: in: query name: page schema: type: integer minimum: 0 default: 0 required: false SizeParam: in: query name: size schema: type: integer minimum: 1 maximum: 100 default: 20 required: false ModeParam: in: query name: mode schema: type: string enum: [LIVE] default: LIVE required: false MessageInternalId: in: path name: internalId required: true schema: { type: string } description: ID interno da mensagem (formato `ara_msg_*`). TemplateId: in: path name: id required: true schema: type: string format: uuid CampaignId: in: path name: id required: true schema: type: string format: uuid CampaignIdPath: in: path name: campaignId required: true schema: type: string format: uuid ApiKeyId: in: path name: id required: true schema: type: string format: uuid NumberId: in: path name: id required: true schema: type: string format: uuid responses: BadRequest: description: Payload inválido content: application/json: schema: $ref: '#/components/schemas/Error' Unauthorized: description: Token ausente ou inválido content: application/json: schema: $ref: '#/components/schemas/Error' Forbidden: description: Sem permissão (plano, escopo, status da conta) content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Recurso não encontrado content: application/json: schema: $ref: '#/components/schemas/Error' Conflict: description: Conflito de estado content: application/json: schema: $ref: '#/components/schemas/Error' UnprocessableEntity: description: Entidade não processável (validação semântica falhou) content: application/json: schema: $ref: '#/components/schemas/Error' TooManyRequests: description: Rate limit excedido — honre header `Retry-After` headers: Retry-After: schema: type: integer description: Segundos a aguardar antes de retentar. content: application/json: schema: $ref: '#/components/schemas/Error' InternalServerError: description: Erro interno do servidor content: application/json: schema: $ref: '#/components/schemas/Error' schemas: # ---------- Error & Pagination ---------- Error: type: object required: [error] properties: error: type: object required: [code, message] properties: code: type: string example: INSUFFICIENT_FUNDS message: type: string example: Saldo insuficiente para enviar a mensagem. details: type: object additionalProperties: true Pagination: type: object properties: page: type: integer minimum: 0 size: type: integer minimum: 1 totalElements: type: integer format: int64 totalPages: type: integer # ---------- Auth ---------- FirebaseLoginRequest: type: object required: [token] properties: token: type: string description: Firebase ID token. UserResponseDTO: type: object required: [name, email] properties: name: { type: string } email: { type: string, format: email } role: { type: string, nullable: true } AuthResponse: type: object required: [user, token, isAuthenticated] properties: user: $ref: '#/components/schemas/UserResponseDTO' token: { type: string } userId: type: string format: uuid nullable: true isAuthenticated: { type: boolean } apiKey: type: string nullable: true OrganizationItem: type: object required: [id, name, role, mode] properties: id: type: string format: uuid slug: type: string nullable: true name: { type: string } role: type: string enum: [OWNER, MEMBER] mode: type: string enum: [live, test] OrganizationListResponse: type: object required: [data] properties: data: type: array items: $ref: '#/components/schemas/OrganizationItem' # ---------- Messages ---------- SendMessageRequest: type: object required: [receiver] description: | Envia exatamente UM tipo de payload: `templateName` OU `body` OU `interactive` OU `location` OU `reaction`. properties: receiver: type: string description: Número E.164 do destinatário. example: '+5511999998888' sender: type: string nullable: true description: Número remetente (opcional; usa default da org). type: type: string nullable: true enum: [template, text, interactive, location, reaction] description: Tipo explícito. Quando omitido, infere de outros campos. templateName: type: string nullable: true templateVariables: type: array items: { type: string } nullable: true description: Lista posicional de variáveis (aceita alias `variables`). smartLinkParam: type: string nullable: true smartLinkUrl: type: string nullable: true body: type: string nullable: true description: Texto livre (apenas em janela 24h aberta). interactive: $ref: '#/components/schemas/InteractivePayload' location: $ref: '#/components/schemas/LocationPayload' reaction: $ref: '#/components/schemas/ReactionPayload' replyTo: type: string nullable: true description: internalId da mensagem original sendo respondida. scheduled_at: type: string format: date-time nullable: true mode: type: string enum: [LIVE] nullable: true media_url: type: string nullable: true deprecated: true description: 'DEPRECATED — use variables posicionalmente (header na primeira posição).' MessageResponse: type: object required: [status, mode, sender, receiver] properties: id: type: string nullable: true status: type: string enum: [PENDING, SCHEDULED, PROCESSING, SENT, DELIVERED, READ, FAILED, CANCELED] example: SENT mode: type: string enum: [LIVE] sender: { type: string } receiver: { type: string } body: type: string nullable: true cost: type: number format: float nullable: true InteractivePayload: type: object required: [type, body] properties: type: type: string enum: [list, button] body: { type: string } buttonText: type: string nullable: true sections: type: array nullable: true items: $ref: '#/components/schemas/InteractiveSection' buttons: type: array nullable: true items: $ref: '#/components/schemas/InteractiveButton' InteractiveSection: type: object required: [title, rows] properties: title: { type: string } rows: type: array items: $ref: '#/components/schemas/InteractiveRow' InteractiveRow: type: object required: [id, title] properties: id: { type: string } title: { type: string } description: type: string nullable: true InteractiveButton: type: object required: [id, title] properties: id: { type: string } title: { type: string } LocationPayload: type: object required: [latitude, longitude] properties: latitude: { type: number, format: double } longitude: { type: number, format: double } name: type: string nullable: true address: type: string nullable: true ReactionPayload: type: object required: [emoji, messageId] properties: emoji: { type: string } messageId: type: string description: internalId da mensagem a reagir. BatchMessageRequest: type: object required: [templateName, messages] properties: templateName: { type: string } messages: type: array maxItems: 1000 items: $ref: '#/components/schemas/BatchMessageItem' BatchMessageItem: type: object required: [receiver] properties: receiver: { type: string } templateVariables: type: array items: { type: string } nullable: true smartLinkParam: type: string nullable: true smartLinkUrl: type: string nullable: true mediaUrl: type: string nullable: true BatchMessageResponse: type: object required: [batchId, templateName, total, accepted, totalCost, messages] properties: batchId: { type: string } templateName: { type: string } total: { type: integer } accepted: { type: integer } totalCost: { type: number, format: float } messages: type: array items: $ref: '#/components/schemas/BatchMessageItemResponse' BatchMessageItemResponse: type: object required: [receiver, status] properties: id: type: string nullable: true receiver: { type: string } status: { type: string } cost: type: number format: float nullable: true # ---------- Templates ---------- CreateTemplateRequest: type: object required: [name, category, body] properties: name: { type: string } category: type: string enum: [MARKETING, UTILITY, AUTHENTICATION] body: { type: string } language: type: string default: pt_BR header: type: string nullable: true headerType: type: string nullable: true enum: [text, media, document] footer: type: string nullable: true buttons: type: array nullable: true items: $ref: '#/components/schemas/TemplateButton' samples: type: object nullable: true additionalProperties: type: string variableExamples: type: array nullable: true items: { type: string } carouselCards: type: array nullable: true minItems: 2 maxItems: 10 items: $ref: '#/components/schemas/CarouselCard' otpButtonType: type: string nullable: true enum: [COPY_CODE, ONE_TAP] otpPackageName: type: string nullable: true otpAppSignature: type: string nullable: true TemplateButton: type: object required: [type, text] properties: type: type: string enum: [QUICK_REPLY, PHONE_NUMBER, URL, SMART_LINK, COPY_CODE] text: { type: string } url: type: string nullable: true phone: type: string nullable: true extraConfig: type: object nullable: true additionalProperties: true CarouselCard: type: object required: [mediaUrl, body] properties: mediaUrl: { type: string } body: { type: string } buttons: type: array nullable: true maxItems: 2 items: $ref: '#/components/schemas/TemplateButton' TemplateResponse: type: object required: - id - name - formattedName - category - language - providerName - providerTemplateId - providerStatus - structureJson - createdAt properties: id: type: string format: uuid name: { type: string } formattedName: { type: string } category: { type: string } originalCategory: type: string nullable: true language: { type: string } providerName: { type: string } providerTemplateId: { type: string } providerStatus: type: string example: APPROVED rejectionReason: type: string nullable: true bodyPreview: type: string nullable: true structureJson: type: object additionalProperties: true usageGuide: type: object nullable: true additionalProperties: true variablesSchema: type: object nullable: true additionalProperties: type: string createdAt: type: string format: date-time updatedAt: type: string format: date-time nullable: true TemplateStatusResponse: type: object required: [status] properties: status: { type: string } rejectionReason: type: string nullable: true category: type: string nullable: true TemplateAnalyticsResponse: type: object required: [templateId, templateName, period, sent, delivered, read, failed, deliveryRate, readRate] properties: templateId: { type: string } templateName: { type: string } period: type: string enum: [7d, 30d, 90d] sent: { type: integer, format: int64 } delivered: { type: integer, format: int64 } read: { type: integer, format: int64 } failed: { type: integer, format: int64 } deliveryRate: type: string description: Percentual formatado com 1 casa decimal (ex. "98.7"). readRate: type: string # ---------- Campaigns ---------- CampaignRequest: type: object required: [name, templateName, contacts] properties: name: { type: string } templateName: { type: string } sender: type: string nullable: true contacts: type: array minItems: 1 items: $ref: '#/components/schemas/CampaignContactRequest' CampaignContactRequest: type: object required: [to] properties: to: type: string description: Telefone E.164. variables: type: array default: [] items: { type: string } CampaignResponse: type: object required: [id, name, status, totalMessages, totalCost] properties: id: { type: string, format: uuid } name: { type: string } status: { type: string } totalMessages: { type: integer } totalCost: { type: number, format: float } CampaignListItem: type: object required: [id, name, status, templateName, totalMessages, sentCount, totalCost] properties: id: { type: string, format: uuid } name: { type: string } status: { type: string } templateName: { type: string } totalMessages: { type: integer } sentCount: { type: integer } totalCost: { type: number, format: float } createdAt: type: string format: date-time nullable: true CampaignListResponse: type: object required: [content, totalPages, totalElements] properties: content: type: array items: $ref: '#/components/schemas/CampaignListItem' totalPages: { type: integer } totalElements: { type: integer, format: int64 } CampaignDetailResponse: type: object required: - id - name - status - templateName - totalMessages - sentCount - deliveredCount - readCount - clickedCount - convertedCount - convertedValue - totalCost properties: id: { type: string, format: uuid } name: { type: string } status: { type: string } templateName: { type: string } templateBody: type: string nullable: true totalMessages: { type: integer } sentCount: { type: integer } deliveredCount: { type: integer } readCount: { type: integer } clickedCount: { type: integer } convertedCount: { type: integer } convertedValue: { type: number, format: float } totalCost: { type: number, format: float } scheduledAt: type: string format: date-time nullable: true startedAt: type: string format: date-time nullable: true finishedAt: type: string format: date-time nullable: true createdAt: type: string format: date-time nullable: true CampaignEstimateResponse: type: object required: [templateCategory, recipientCount, templateCost, araraFee, unitPrice, totalCost] properties: templateCategory: type: string enum: [MARKETING, UTILITY, AUTHENTICATION] recipientCount: { type: integer } templateCost: { type: number, format: float } araraFee: { type: number, format: float } unitPrice: { type: number, format: float } totalCost: { type: number, format: float } CreateAbTestRequest: type: object required: [variantATemplateName, variantBTemplateName, metric] properties: variantATemplateName: { type: string } variantBTemplateName: { type: string } metric: type: string enum: [DELIVERY_RATE, READ_RATE, CLICK_RATE, CONVERSION_RATE] samplePct: type: integer minimum: 1 maximum: 100 default: 20 splitPct: type: integer minimum: 1 maximum: 99 default: 50 decisionWindowMinutes: type: integer default: 240 autopilot: type: boolean default: false ForceWinnerRequest: type: object required: [winner] properties: winner: type: string enum: [A, B] AbTestResponse: type: object required: [variantATemplateName, variantBTemplateName, metric, samplePct, splitPct, decisionWindowMinutes, autopilot] properties: id: type: string format: uuid nullable: true campaignId: type: string format: uuid nullable: true variantATemplateName: { type: string } variantBTemplateName: { type: string } metric: { type: string } samplePct: { type: integer } splitPct: { type: integer } decisionWindowMinutes: { type: integer } autopilot: { type: boolean } winner: type: string nullable: true enum: [A, B] decidedAt: type: string format: date-time nullable: true decidedReason: type: string nullable: true pValue: type: number format: float nullable: true relativeLiftPct: type: number format: float nullable: true variantAMetricsJson: type: string nullable: true variantBMetricsJson: type: string nullable: true createdAt: type: string format: date-time nullable: true # ---------- Contacts ---------- ContactRequest: type: object required: [name, phone] properties: name: type: string maxLength: 255 phone: type: string pattern: '^\+[1-9]\d{6,14}$' example: '+5511999998888' email: type: string format: email maxLength: 255 nullable: true attributes: type: object nullable: true additionalProperties: true ContactResponse: type: object required: [id, name, phone, createdAt] properties: id: { type: string, format: uuid } name: { type: string } phone: { type: string } email: type: string nullable: true attributes: type: object nullable: true additionalProperties: true createdAt: { type: string } ContactsListResponse: type: object required: [contacts, total, page, size, totalPages] properties: contacts: type: array items: $ref: '#/components/schemas/ContactResponse' total: { type: integer, format: int64 } page: { type: integer } size: { type: integer } totalPages: { type: integer } ContactsBatchResponse: type: object required: [importId, created, updated, skipped] properties: importId: { type: string, format: uuid } created: { type: integer } updated: { type: integer } skipped: { type: integer } errors: type: array default: [] items: $ref: '#/components/schemas/ContactsBatchError' ContactsBatchError: type: object required: [index, reason] properties: index: { type: integer } phone: type: string nullable: true reason: { type: string } # ---------- API Keys ---------- CreateApiKeyRequest: type: object properties: name: type: string description: Nome legível da chave (ex. "Site WordPress"). scope: type: string enum: [ADMIN, SEND_ONLY, READ_ONLY] expiresAt: type: string format: date-time nullable: true ipAllowlist: type: string nullable: true description: Lista de IPs/CIDRs separada por vírgula (feature Business). UpdateApiKeyRequest: type: object properties: name: type: string nullable: true scope: type: string nullable: true expiresAt: type: string format: date-time nullable: true ipAllowlist: type: string nullable: true clearExpiration: type: boolean default: false clearIpAllowlist: type: boolean default: false GeneratedApiKey: type: object required: [plainTextKey] properties: plainTextKey: type: string description: Chave em texto plano. **Só aparece uma vez** — guarde antes de fechar a tela. ApiKeyListItem: type: object required: [id, name, prefix, mode, scope, createdAt] properties: id: { type: string, format: uuid } name: { type: string } prefix: { type: string, example: ara_live_ } lastFour: type: string nullable: true mode: type: string enum: [LIVE] scope: type: string createdAt: { type: string, format: date-time } lastUsedAt: type: string format: date-time nullable: true expiresAt: type: string format: date-time nullable: true expired: { type: boolean } ipAllowlist: type: string nullable: true rotatedFromId: type: string format: uuid nullable: true ApiKeyStatsResponse: type: object required: [totalLast7d, totalLast30d, successLast30d] properties: totalLast7d: { type: integer, format: int64 } totalLast30d: { type: integer, format: int64 } successLast30d: { type: integer, format: int64 } successRateLast30d: type: number format: float nullable: true ApiKeyEvent: type: object required: [id, type] properties: id: { type: string, format: uuid } type: type: string example: API_KEY_CREATED actorUserId: type: string format: uuid nullable: true metadata: type: string nullable: true createdAt: type: string format: date-time nullable: true # ---------- Conversations ---------- ConversationListItem: type: object properties: id: { type: string, format: uuid } customerPhone: { type: string } customerName: type: string nullable: true status: { type: string } lastInteractionAt: type: string format: date-time nullable: true windowExpiresAt: type: string format: date-time nullable: true isWindowOpen: { type: boolean } createdAt: type: string format: date-time nullable: true leadStatus: type: string nullable: true leadScore: type: integer nullable: true leadSummary: type: string nullable: true originatingCampaignId: type: string format: uuid nullable: true qualifierCompletedAt: type: string format: date-time nullable: true ConversationListResponse: type: object required: [content, page, size, totalElements, totalPages] properties: content: type: array items: $ref: '#/components/schemas/ConversationListItem' page: { type: integer } size: { type: integer } totalElements: { type: integer, format: int64 } totalPages: { type: integer } ConversationMessage: type: object properties: id: { type: string } direction: type: string enum: [INBOUND, OUTBOUND] body: type: string nullable: true status: { type: string } sender: { type: string } receiver: { type: string } messageType: type: string nullable: true createdAt: type: string format: date-time nullable: true mediaUrl: type: string nullable: true ConversationMessagesResponse: type: object required: [content, page, size, totalElements, totalPages] properties: content: type: array items: $ref: '#/components/schemas/ConversationMessage' page: { type: integer } size: { type: integer } totalElements: { type: integer, format: int64 } totalPages: { type: integer } ConversationReplyRequest: type: object required: [conversationId, body] properties: conversationId: { type: string, format: uuid } body: { type: string } ConversationReplyResponse: type: object required: [status] properties: id: type: string nullable: true status: { type: string } message: type: string nullable: true WindowStatusRequest: type: object required: [phones] properties: phones: type: array maxItems: 200 items: { type: string } WindowStatusItem: type: object required: [phone, isWindowOpen] properties: phone: { type: string } isWindowOpen: { type: boolean } windowExpiresAt: type: string format: date-time nullable: true hoursRemaining: type: number format: double nullable: true conversationId: type: string format: uuid nullable: true WindowStatusResponse: type: object required: [results, truncated] properties: results: type: array items: $ref: '#/components/schemas/WindowStatusItem' truncated: type: boolean description: True quando a lista excedeu 200 telefones e foi truncada. # ---------- Webhook config ---------- WebhookConfigResponse: type: object properties: url: type: string secret: type: string isSharedNumber: { type: boolean } consecutiveFailures: { type: integer } lastSuccessAt: type: string format: date-time nullable: true disabledAt: type: string format: date-time nullable: true disabledReason: type: string nullable: true UpdateWebhookRequest: type: object properties: url: type: string nullable: true secret: type: string nullable: true CartAbandonedWebhookRequest: type: object required: [event, phone, checkout_url] properties: event: type: string enum: [cart.abandoned, payment.failed, pix.created, boleto.due] name: type: string nullable: true phone: type: string total: type: number format: double nullable: true checkout_url: type: string minutes_without_payment: type: integer nullable: true pix_qr_code: type: string nullable: true description: Obrigatório quando event=pix.created. CartAbandonedWebhookResponse: type: object required: [status, received_at] properties: status: { type: string } message_id: type: string format: uuid nullable: true received_at: type: string format: date-time # ---------- Wallet ---------- RechargeEstimateLimits: type: object required: [minCents, maxCents] properties: minCents: { type: integer } maxCents: { type: integer } RechargeEstimateBonus: type: object required: [applicable, percent, bonusCents, totalCreditCents] properties: applicable: { type: boolean } percent: { type: integer } bonusCents: { type: integer } totalCreditCents: { type: integer } CategoryEstimate: type: object required: [count, unitPriceCents] properties: count: { type: integer } unitPriceCents: { type: integer } RechargeEstimate: type: object required: [amountCents, planLabel, limits, firstRechargeBonus, estimates] properties: amountCents: { type: integer } planLabel: { type: string } limits: $ref: '#/components/schemas/RechargeEstimateLimits' firstRechargeBonus: $ref: '#/components/schemas/RechargeEstimateBonus' estimates: type: object additionalProperties: $ref: '#/components/schemas/CategoryEstimate' AutoRechargeSettings: type: object required: [enabled, threshold, amount] properties: enabled: { type: boolean } threshold: type: number format: float description: Saldo mínimo (R$) que dispara a recarga. amount: type: number format: float description: Valor da recarga (R$). lastAttemptAt: type: string format: date-time nullable: true lastFailureReason: type: string nullable: true UpdateAutoRechargeRequest: type: object properties: enabled: type: boolean nullable: true threshold: type: number format: float nullable: true minimum: 5 maximum: 1000 amount: type: number format: float nullable: true minimum: 20 maximum: 5000 WalletTransaction: type: object required: [id, amount, type, mode] properties: id: { type: string } amount: type: number format: float type: type: string enum: [RECHARGE, DEBIT, REFUND, BONUS, ADJUSTMENT] description: type: string nullable: true referenceId: type: string nullable: true mode: type: string enum: [LIVE] createdAt: type: string format: date-time nullable: true WalletTransactionPage: type: object required: [content, page, size, totalElements, totalPages] properties: content: type: array items: $ref: '#/components/schemas/WalletTransaction' page: { type: integer } size: { type: integer } totalElements: { type: integer, format: int64 } totalPages: { type: integer } # ---------- Payments ---------- CreatePaymentRequest: type: object properties: planType: type: string enum: [FREE, ESSENTIAL, PRO, ENTERPRISE] nullable: true isRecharge: type: boolean default: false amountCentavos: type: integer nullable: true taxId: type: string nullable: true description: CPF/CNPJ. cellphone: type: string nullable: true AbacatePayPixData: type: object required: [id, amount, status, brCode, brCodeBase64, expiresAt] properties: id: { type: string } amount: { type: integer } status: { type: string } brCode: type: string description: Código copia-e-cola do PIX. brCodeBase64: type: string description: QR code (PNG base64). expiresAt: { type: string } AbacatePayPixResponse: type: object required: [data] properties: data: $ref: '#/components/schemas/AbacatePayPixData' error: type: string nullable: true AbacatePayStatusData: type: object required: [status, expiresAt] properties: status: { type: string } expiresAt: { type: string } AbacatePayStatusResponse: type: object required: [data] properties: data: $ref: '#/components/schemas/AbacatePayStatusData' error: type: string nullable: true PaymentRequestDTO: type: object required: [planKey] properties: planKey: { type: string } billingCycle: type: string enum: [MONTHLY, YEARLY] default: MONTHLY uiMode: type: string enum: [hosted, embedded] default: hosted rechargeAmountCents: type: integer nullable: true description: Topup one-time cobrado junto com o primeiro invoice da assinatura. CheckoutSessionResponse: type: object properties: url: type: string nullable: true description: Populado quando uiMode=hosted. clientSecret: type: string nullable: true description: Populado quando uiMode=embedded. # ---------- Numbers ---------- NumberCard: type: object required: - id - name - phoneNumber - type - isDefault - status - qualityScore - messagingTier - provider properties: id: { type: string } name: { type: string } alias: type: string nullable: true description: type: string nullable: true phoneNumber: { type: string } type: type: string enum: [SHARED, DEDICATED] isDefault: { type: boolean } status: { type: string } qualityScore: type: string enum: [UNKNOWN, GREEN, YELLOW, RED, LOW, HIGH, FLAGGED] messagingTier: type: string enum: [TIER_50, TIER_250, TIER_1K, TIER_10K, TIER_100K, UNLIMITED] verifiedAt: type: string format: date-time nullable: true lastHealthCheckAt: type: string format: date-time nullable: true provider: { type: string, example: TWILIO } createdAt: type: string format: date-time nullable: true messagesLast7d: { type: integer, format: int64 } messagesLast30d: { type: integer, format: int64 } NumbersSlot: type: object required: [used, max, planLabel, atCap, noEntitlement, monthlyPriceCents, monthlyTotalCents] properties: used: { type: integer } max: type: integer description: -1 = ilimitado. planLabel: { type: string } atCap: { type: boolean } noEntitlement: { type: boolean } monthlyPriceCents: { type: integer } monthlyTotalCents: { type: integer } NumbersResponse: type: object required: [numbers, slot] properties: numbers: type: array items: $ref: '#/components/schemas/NumberCard' slot: $ref: '#/components/schemas/NumbersSlot' UpdateNumberRequest: type: object properties: alias: type: string nullable: true isDefault: type: boolean nullable: true name: type: string nullable: true description: type: string nullable: true RequestNumberRequest: type: object properties: reason: type: string nullable: true maxLength: 500 expectedVolume: type: string nullable: true maxLength: 500 areaCode: type: string nullable: true maxLength: 4 description: DDD preferido (ex. "11"). displayName: type: string nullable: true maxLength: 80 profilePictureUrl: type: string nullable: true maxLength: 500 RequestNumberResponse: type: object required: [status, requestId] properties: status: { type: string, example: received } requestId: { type: string, format: uuid } message: { type: string } NumberRequestItem: type: object properties: id: { type: string, format: uuid } areaCode: type: string nullable: true displayName: type: string nullable: true profilePictureUrl: type: string nullable: true reason: type: string nullable: true expectedVolume: type: string nullable: true status: type: string enum: [PENDING, IN_PROGRESS, FULFILLED, REJECTED] createdAt: type: string format: date-time nullable: true fulfilledAt: type: string format: date-time nullable: true NumberSyncResponse: type: object required: [matched] properties: matched: type: boolean description: True quando o sender foi encontrado na Twilio. qualityScore: { type: string } messagingTier: { type: string } status: { type: string } lastHealthCheckAt: type: string format: date-time nullable: true NumberWarmingResponse: type: object properties: numberId: { type: string, format: uuid } qualityScore: { type: string } messagingTier: { type: string } daysSinceVerified: { type: integer, format: int64 } dailyCapByTier: type: integer description: -1 = sem cap (UNLIMITED). recommendedDailySends: { type: integer } recommendedPacePct: { type: integer } currentDailyAverage: { type: integer } recommendation: { type: string } # ---------- Business Profile ---------- BusinessProfilePatch: type: object properties: displayName: type: string nullable: true maxLength: 120 vertical: type: string nullable: true enum: - AUTO - BEAUTY - APPAREL - EDU - ENTERTAIN - EVENT_PLAN - FINANCE - GROCERY - GOVT - HOTEL - HEALTH - NONPROFIT - PROF_SERVICES - RETAIL - TRAVEL - RESTAURANT - NOT_A_BIZ - OTHER description: type: string nullable: true aboutShort: type: string nullable: true maxLength: 140 email: type: string format: email nullable: true maxLength: 160 websites: type: array nullable: true items: { type: string } businessHours: type: object nullable: true additionalProperties: true profilePhotoUrl: type: string nullable: true awayMessage: type: string nullable: true BusinessProfileResponse: type: object required: [metaSyncStatus] properties: id: type: string format: uuid nullable: true displayName: type: string nullable: true vertical: type: string nullable: true description: type: string nullable: true aboutShort: type: string nullable: true email: type: string nullable: true websites: type: array nullable: true items: { type: string } businessHours: type: object nullable: true additionalProperties: true profilePhotoUrl: type: string nullable: true awayMessage: type: string nullable: true metaSyncStatus: type: string enum: [PENDING, SYNCED, FAILED, PARTIAL, NO_SENDER] metaSyncedAt: type: string format: date-time nullable: true metaSyncError: type: string nullable: true updatedAt: type: string format: date-time nullable: true BusinessProfileSyncResponse: type: object properties: profile: $ref: '#/components/schemas/BusinessProfileResponse' twilioSendersPushed: { type: integer } twilioSendersFailed: { type: integer } totalSenders: { type: integer } # ---------- Profile / LGPD ---------- ProfileResponse: type: object properties: id: type: string nullable: true name: type: string nullable: true email: { type: string, format: email } phoneNumber: type: string nullable: true alertPhoneNumber: type: string nullable: true avatarUrl: type: string nullable: true timezone: type: string nullable: true locale: type: string nullable: true companyName: type: string nullable: true documentNumber: type: string nullable: true orgRole: { type: string } canEditCompany: { type: boolean } accountStatus: { type: string } createdAt: { type: string } lastLoginAt: type: string nullable: true mfa: type: object properties: enabled: { type: boolean } enabledAt: type: string nullable: true notifications: type: object properties: marketing: { type: boolean } billing: { type: boolean } product: { type: boolean } emailChange: type: object properties: pendingTo: type: string nullable: true requestedAt: type: string nullable: true ProfilePatch: type: object description: Campos editáveis pelo próprio usuário. Tudo opcional — só envia o que muda. properties: name: type: string nullable: true phoneNumber: type: string nullable: true alertPhoneNumber: type: string nullable: true avatarUrl: type: string nullable: true timezone: type: string nullable: true locale: type: string nullable: true companyName: type: string nullable: true documentNumber: type: string nullable: true notifEmailMarketing: type: boolean nullable: true notifEmailBilling: type: boolean nullable: true notifEmailProduct: type: boolean nullable: true DeleteAccountRequest: type: object required: [confirmation] properties: confirmation: type: string description: Deve ser exatamente `EXCLUIR `. # ---------- Recovery ---------- RecoveryEndpointResponse: type: object required: [code, hmacSecret, active, url] properties: code: { type: string } hmacSecret: type: string description: Segredo HMAC pra assinatura de webhooks. Tratar como secret. active: { type: boolean } url: type: string description: URL pública de ingestão (`/l/{code}`). RecoveryEndpointPatchRequest: type: object properties: active: type: boolean nullable: true RecoveryEventConfigResponse: type: object required: [eventType, active] properties: eventType: { type: string } templateId: type: string format: uuid nullable: true templateName: type: string nullable: true active: { type: boolean } lastTriggeredAt: type: string format: date-time nullable: true RecoveryEventConfigPatchRequest: type: object properties: templateId: type: string format: uuid nullable: true active: type: boolean nullable: true variableMapping: type: object nullable: true additionalProperties: type: string RecoveryEventsListResponse: type: object required: [data] properties: data: type: array items: $ref: '#/components/schemas/RecoveryEventConfigResponse' RecoveryIngestResult: type: object required: [ingestId, status] properties: ingestId: { type: string } status: { type: string } reason: type: string nullable: true messageId: type: string nullable: true # ---------- Pricing ---------- PricingCategoryRow: type: object required: [category, priceBrl, source] properties: category: type: string enum: [MARKETING, UTILITY, AUTHENTICATION] priceBrl: type: number format: float source: type: string enum: [ORG_CUSTOM, GLOBAL_DEFAULT] PricingCountryRow: type: object required: [countryPrefix, countryName, category, baseCostUsd] properties: countryPrefix: { type: string } countryName: { type: string } category: { type: string } baseCostUsd: type: number format: float PricingResponse: type: object required: [currency, model, categories, countries] properties: currency: { type: string, example: BRL } model: { type: string, example: PER_MESSAGE } categories: type: array items: $ref: '#/components/schemas/PricingCategoryRow' countries: type: array items: $ref: '#/components/schemas/PricingCountryRow' # ---------- Organization plan ---------- PlanInfoResponse: type: object required: - current - monthlyPriceCents - marketingPriceCents - utilityPriceCents - authPriceCents - features properties: current: type: string enum: [FREE, ESSENTIAL, PRO, ENTERPRISE] monthlyPriceCents: { type: integer } marketingPriceCents: { type: integer } utilityPriceCents: { type: integer } authPriceCents: { type: integer } features: type: array items: { type: string } nextBillingAt: type: string format: date-time nullable: true # ---------- Dashboard ---------- DashboardMessageItem: type: object properties: id: { type: string } status: { type: string } mode: type: string enum: [LIVE] sender: { type: string } receiver: { type: string } messageType: { type: string } templateName: { type: string } templateSid: { type: string } templateLanguage: { type: string } providerSid: { type: string } errorDetails: { type: string } scheduledAt: { type: string } externalReference: { type: string } deliveredAt: { type: string } readAt: { type: string } createdAt: { type: string } updatedAt: { type: string } cost: type: number format: float DashboardMessagesResponse: type: object required: [messages, total, page, size, totalPages] properties: messages: type: array items: $ref: '#/components/schemas/DashboardMessageItem' total: { type: integer, format: int64 } page: { type: integer } size: { type: integer } totalPages: { type: integer } DashboardMetricsResponse: type: object required: [total, sent, delivered, read, failed, pending, deliveryRate, mode] properties: total: { type: integer, format: int64 } sent: { type: integer, format: int64 } delivered: { type: integer, format: int64 } read: { type: integer, format: int64 } failed: { type: integer, format: int64 } pending: { type: integer, format: int64 } deliveryRate: type: string description: Percentual com 1 casa decimal (string). totalCost: type: number format: float nullable: true mode: type: string enum: [LIVE] WalletBalanceResponse: type: object required: [balance, currency, mode] properties: balance: type: number format: double currency: type: string example: BRL mode: type: string enum: [LIVE] # ---------- Health ---------- HealthResponse: type: object required: [status, timestamp, service] properties: status: type: string example: UP timestamp: type: string format: date-time service: type: string example: arara-api