openapi: 3.0.3 info: title: API da Arara description: | API REST da Arara para WhatsApp Business: mensagens, templates, campanhas, contatos, conversas, formulários, automações, smart links e carteira. Esta especificação cobre só os endpoints que aceitam **chave de API** (`Authorization: Bearer ara_live_...`). Rotas usadas apenas pelo painel (gestão de chaves, configuração do webhook, pagamentos e perfil pessoal) ficam de fora. ## Autenticação Header `Authorization: Bearer ara_live_...`. Crie a chave em https://app.ararahq.com/dashboard/apikeys. As chaves são sempre LIVE: mensagens são entregues de verdade e o custo sai da carteira. ## Permissões da chave Chave criada sem permissões explícitas tem acesso total (`ADMIN`). Chave com permissões restritas só alcança: - `READ`: `GET` em `/v1/messages`, `/v1/campaigns`, `/v1/templates`, `/v1/flows`, `/v1/automations` e `/v1/organizations/me/numbers`. - `MESSAGES_SEND`: escrita em `/v1/messages/**`, `POST /v1/media/upload` e `POST /v1/flows/{id}/test`. - `TEMPLATES_WRITE`: escrita em `/v1/templates/**` e `/v1/flows/**`. - `CAMPAIGNS_SEND`: escrita em `/v1/campaigns/**`. - `CONTACTS_WRITE`: escrita em `/v1/contacts/**`. - `AUTOMATIONS_MANAGE`: escrita em `/v1/automations/**`. - `WEBHOOKS_MANAGE`: escrita em `/webhook/**`. Qualquer outra rota exige `ADMIN`. Fora do escopo, a resposta é `403`. ## Rate limit Por usuário da chave: 1.200 leituras (`GET`) por minuto e 3.000 escritas por minuto. Toda resposta traz `X-RateLimit-Remaining`. Acima do limite: `429` com `Retry-After` e `error.code = RATE_LIMIT_EXCEEDED`. O envio tem limites próprios (`SEND_RATE_LIMITED`, `BATCH_BUSY`, `MARKETING_FREQUENCY_EXCEEDED`). ## Formato de erro ```json { "error": { "code": "ERROR_CODE", "message": "Descrição", "details": {} } } ``` Exceções: falha de autenticação (`401`, `403` e `503` do filtro de chave) volta no formato padrão do Spring (`timestamp`, `status`, `error`, `path`, sem `code`), e alguns `404` e `403` voltam sem corpo. version: 2.1.0 contact: name: AraraHQ url: https://ararahq.com email: oi@ararahq.com servers: - url: https://api.ararahq.com description: Produção security: - bearerAuth: [] tags: - name: Mensagens description: Envio, consulta e histórico de mensagens de WhatsApp, e upload de mídia. - name: Conversas description: Conversas com cada telefone, resposta dentro da janela de 24h e situação da janela. - name: Templates description: Criação, listagem, status de aprovação e métricas de templates. - name: Campanhas description: Envio do mesmo template para muitos contatos, com estimativa, agendamento, cancelamento e teste A/B. - name: Contatos description: Base de contatos da organização e direitos do titular (LGPD). - name: Opt-out description: Registro e consulta de quem pediu para não receber mensagens. - name: Formulários description: Formulários nativos do WhatsApp (Flows) e as respostas recebidas. Disponíveis do plano Voo para cima. - name: Automações description: Sequências de passos que rodam sozinhas para cada contato que entra por um gatilho. Exigem plano com recuperação de receita. - name: Smart Links description: Links wa.me rastreáveis, com QR code. - name: Números description: Números de WhatsApp da organização e o perfil comercial exibido no WhatsApp. - name: Carteira description: Saldo, extrato, recarga automática e preços. - name: Consulta de telefone description: Consulta de tipo de linha e de WhatsApp de um número (cobrada). - name: Recuperação description: Endpoint de recuperação de receita (carrinho abandonado, Pix, boleto). - name: Conta description: Identidade da chave, plano da organização e status da API. paths: # ---------------------- MENSAGENS ---------------------- /v1/messages: post: tags: [Mensagens] operationId: sendMessage summary: Enviar mensagem description: | Envia uma mensagem de WhatsApp. Mande exatamente **um** conteúdo: `templateName`, `body`, `interactive`, `location` ou `reaction`. Zero ou mais de um responde `400 VALIDATION_ERROR`. - Template aprovado (`templateName`) pode ser enviado a qualquer hora. - Os demais conteúdos só passam com a janela de 24h aberta. Fora dela: `422 CONVERSATION_WINDOW_CLOSED`. A resposta `202` traz o `id` da mensagem. O status final chega pelo webhook `message.status_updated`. `Idempotency-Key` (opcional): a mesma chave, para o mesmo usuário, devolve a mensagem já criada, sem novo envio nem nova cobrança. A chave não expira e o corpo **não** é comparado: reusar a chave com outro destinatário devolve a mensagem antiga, sem erro. Use uma chave por intenção de envio, derivada de um evento do seu sistema (ex. `pedido-12345-confirmacao`). Permissão: `MESSAGES_SEND`. parameters: - $ref: '#/components/parameters/IdempotencyKeyOptional' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SendMessageRequest' examples: template: summary: Template aprovado value: receiver: '+5511999999999' templateName: pedido_confirmado templateVariables: [João, '12345'] texto: summary: Texto na janela de 24h value: receiver: '+5511999999999' body: Já vou te responder, só preciso checar uma coisa. midia: summary: Template com mídia no cabeçalho value: receiver: '+5511999999999' templateName: fatura_disponivel templateVariables: ['https://meusite.com/fatura.pdf', João, Janeiro] agendada: summary: Mensagem agendada value: receiver: '+5511999999999' templateName: lembrete_consulta templateVariables: [amanhã às 14h] scheduled_at: '2027-01-15T14:00:00-03:00' botoes: summary: Botões de resposta value: receiver: '+5511999999999' interactive: type: button body: Qual a sua dúvida? buttons: - {id: rastreio, title: Rastrear pedido} - {id: troca, title: Trocar produto} lista: summary: Lista de opções value: receiver: '+5511999999999' interactive: type: list body: Escolha o setor buttonText: Ver setores sections: - title: Atendimento rows: - id: vendas title: Vendas description: Planos e preços - {id: suporte, title: Suporte} formulario: summary: Formulário value: receiver: '+5511999999999' interactive: type: flow flowId: 0b7e6a52-3c1d-4f7a-9a55-2f4f6f1f9c10 body: Pode responder rapidinho por aqui? buttonText: Responder localizacao: summary: Localização value: receiver: '+5511999999999' location: latitude: -23.5613 longitude: -46.6565 name: Loja Paulista address: Av. Paulista, 1000 reacao: summary: Reação value: receiver: '+5511999999999' reaction: emoji: "👍" messageId: ara_msg_k9Bf2Xq responses: '202': description: Mensagem aceita content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '402': {$ref: '#/components/responses/PaymentRequired'} '403': description: | - Autenticação (corpo padrão do Spring): header ausente ou chave sem `MESSAGES_SEND`. - `ACCOUNT_NOT_ACTIVATED`: conta ainda não ativada. Com `details.reason = FIRST_SUBSCRIPTION_REQUIRED`, falta a primeira assinatura. - `ORGANIZATION_SUSPENDED`, `FRAUD_SUSPECTED`, `PLAN_FEATURE_LOCKED`. - `NUMBER_BLOCKED`: destinatário que falhou repetidas vezes (inválido ou sem WhatsApp). content: application/json: schema: anyOf: - $ref: '#/components/schemas/Error' - $ref: '#/components/schemas/SpringError' example: error: code: ACCOUNT_NOT_ACTIVATED message: Ative a conta para enviar mensagens. details: { reason: FIRST_SUBSCRIPTION_REQUIRED } '409': description: | - `CONFLICT`: template não encontrado na organização. - `PROVIDER_NOT_RESOLVED`: o número de envio não está pronto para enviar. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: CONFLICT message: Template 'pedido_confirmado' não encontrado. details: {} '422': description: | - `INVALID_RECIPIENT`: telefone inválido (precisa ter de 7 a 15 dígitos). - `RECIPIENT_OPTED_OUT`: o destinatário pediu para não receber. - `CONVERSATION_WINDOW_CLOSED`: conteúdo fora de template com a janela de 24h fechada. - `TEMPLATE_PAUSED`, `TEMPLATE_NOT_SENDABLE`, `TEMPLATE_VARIABLES_MISMATCH`: template pausado, não aprovado ou com número de variáveis diferente. - `SENDER_NOT_ALLOWED`: `sender` não pertence à organização ou não pode ser usado. - `UNPROCESSABLE_ENTITY`: outras regras de envio sem código próprio. - `ararahq-*`: falha reconhecida do provedor, com `details.diagnostic`. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: CONVERSATION_WINDOW_CLOSED message: A janela de 24h está fechada. Envie um template. details: {} '429': {$ref: '#/components/responses/TooManyRequests'} '503': {$ref: '#/components/responses/ServiceUnavailable'} get: tags: [Mensagens] operationId: getBatchMessages summary: Listar mensagens de um lote description: | Devolve as mensagens criadas por `POST /v1/messages/batch`, no mesmo formato do envio. `batchId` é obrigatório. Para o histórico completo da organização, use `GET /dashboard/messages`. Permissão: `READ`. parameters: - in: query name: batchId required: true schema: { type: string } description: O `batchId` devolvido por `POST /v1/messages/batch`. responses: '200': description: Mensagens do lote content: application/json: schema: type: array items: $ref: '#/components/schemas/MessageResponse' example: - id: ara_msg_a1B2c3D status: DELIVERED mode: LIVE sender: whatsapp:+5511888888888 receiver: whatsapp:+5511999999999 body: Oi João, seu pedido 1 foi confirmado! cost: 0.068 reason: null '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/messages/batch: post: tags: [Mensagens] operationId: sendBatchMessages summary: Enviar lote de mensagens description: | Envia o mesmo template para até 1.000 destinatários em uma chamada. Para mais que isso, use Campanhas. O lote é tudo ou nada: um telefone inválido ou saldo insuficiente recusa o lote inteiro com `422 UNPROCESSABLE_ENTITY`. Com `Idempotency-Key`, repetir a chamada devolve o lote original sem reenviar. Acompanhe o lote em `GET /v1/messages?batchId=`. Permissão: `MESSAGES_SEND`. parameters: - $ref: '#/components/parameters/IdempotencyKeyOptional' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BatchMessageRequest' example: templateName: pedido_confirmado messages: - receiver: '+5511999999999' templateVariables: [João, '1'] - receiver: '+5511888888888' templateVariables: [Maria, '2'] responses: '202': description: Lote aceito content: application/json: schema: $ref: '#/components/schemas/BatchMessageResponse' '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '409': description: | - `CONFLICT`: template não encontrado na organização. - `PROVIDER_NOT_RESOLVED`: o número de envio não está pronto para enviar. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: CONFLICT message: Template 'pedido_confirmado' não encontrado. details: {} '422': description: '`UNPROCESSABLE_ENTITY`: algum telefone inválido ou saldo insuficiente para o lote. Nada é enviado. Também os códigos de template (`TEMPLATE_PAUSED`, `TEMPLATE_NOT_SENDABLE`, `TEMPLATE_VARIABLES_MISMATCH`).' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: UNPROCESSABLE_ENTITY message: Saldo insuficiente para o lote. details: {} '429': {$ref: '#/components/responses/TooManyRequests'} /v1/messages/{id}: get: tags: [Mensagens] operationId: getMessage summary: Consultar mensagem description: | Devolve a mensagem no mesmo formato do envio, com `status` e `reason` atualizados. Em produção, prefira o webhook `message.status_updated` a consultar em laço. Só devolve mensagens enviadas pelo mesmo usuário dono da chave. Mensagem de outro membro da organização responde `403` sem corpo, e ID inexistente responde `404` sem corpo. Para qualquer mensagem da organização, use `GET /dashboard/messages/{id}`. Permissão: `READ`. parameters: - in: path name: id required: true schema: type: string example: ara_msg_k9Bf2Xq description: ID da mensagem, como veio no campo `id` do envio (ex. `ara_msg_k9Bf2Xq`). responses: '200': description: Mensagem content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': description: Mensagem de outro usuário (sem corpo) ou chave sem permissão. '404': description: Mensagem não encontrada (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} /v1/media/upload: post: tags: [Mensagens] operationId: uploadMedia summary: Enviar arquivo de mídia description: | Sobe um arquivo para a Arara quando ele não está hospedado em outro lugar. Aceita imagem até 5 MB e PDF até 100 MB. Use a `url` devolvida na primeira posição de `templateVariables` de um template com mídia no cabeçalho. Permissão: `MESSAGES_SEND`. requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: '200': description: Arquivo salvo content: application/json: schema: type: object required: [url, directUrl] properties: url: type: string description: Link curto público do arquivo. directUrl: type: string example: url: https://api.ararahq.com/l/Ab3dE9x directUrl: https://storage.exemplo.com/fatura.pdf '400': description: 'Tipo ou tamanho inválido. Corpo `{ "error": "mensagem" }`.' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /dashboard/messages: get: tags: [Mensagens] operationId: getDashboardMessages summary: Listar histórico de mensagens description: | Envelope `{ messages, total, page, size, totalPages }`. Aqui o `status` sai sem tradução (inclui `SENT_TO_PROVIDER`). Permissão: `ADMIN`. parameters: - in: query name: status required: false schema: type: string enum: [all, delivered, sent, read, failed, pending] - in: query name: q required: false schema: { type: string } description: Busca textual. - in: query name: page required: false schema: { type: integer, minimum: 0, default: 0 } - in: query name: size required: false schema: { type: integer, minimum: 1, default: 50 } responses: '200': description: Página de mensagens content: application/json: schema: $ref: '#/components/schemas/DashboardMessagesResponse' '400': description: 'Filtro inválido. Corpo `{ "error": "mensagem" }`.' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /dashboard/messages/{id}: get: tags: [Mensagens] operationId: getDashboardMessageDetail summary: Consultar mensagem do histórico description: | Diferente de `GET /v1/messages/{id}`, vale para qualquer mensagem da organização e inclui variáveis e corpo. Permissão: `ADMIN`. parameters: - in: path name: id required: true schema: { type: string } responses: '200': description: Detalhe content: application/json: schema: $ref: '#/components/schemas/DashboardMessageDetail' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: 'Não encontrada. Corpo `{ "error": "Message not found" }`.' '429': {$ref: '#/components/responses/RateLimited'} /dashboard/metrics: get: tags: [Mensagens] operationId: getDashboardMetrics summary: Consultar métricas de envio description: 'Totais de envio da organização por janela de tempo. Permissão: `ADMIN`.' responses: '200': description: Métricas content: application/json: schema: $ref: '#/components/schemas/DashboardMetricsResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- CONVERSAS ---------------------- /v1/conversations: get: tags: [Conversas] operationId: listConversations summary: Listar conversas description: | Exige plano com caixa de entrada e número dedicado. Envelope `{ content, page, size, totalElements, totalPages }`. Permissão: `ADMIN`. parameters: - in: query name: status required: false schema: type: string enum: [OPEN, CLOSED, ARCHIVED] description: Filtra por status da conversa. - in: query name: leadStatus required: false schema: { type: string } description: Filtra pela qualificação do contato. - in: query name: window required: false schema: type: string enum: [OPEN, EXPIRED] description: '`OPEN` só conversas respondíveis; `EXPIRED` só as que exigem template.' - in: query name: page required: false schema: { type: integer, minimum: 0, default: 0 } - in: query name: size required: false schema: { type: integer, minimum: 1, default: 20 } - $ref: '#/components/parameters/WabaIdHeader' responses: '200': description: Página de conversas content: application/json: schema: $ref: '#/components/schemas/ConversationListResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/conversations/{conversationId}/messages: get: tags: [Conversas] operationId: listConversationMessages summary: Listar mensagens da conversa description: | `page=0` traz as mais recentes; dentro da página a ordem é cronológica. Permissão: `ADMIN`. parameters: - in: path name: conversationId required: true schema: { type: string, format: uuid } - in: query name: page required: false schema: { type: integer, minimum: 0, default: 0 } - in: query name: size required: false schema: { type: integer, minimum: 1, default: 50 } responses: '200': description: Página de mensagens content: application/json: schema: $ref: '#/components/schemas/ConversationMessagesResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: Conversa não encontrada (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} /v1/conversations/reply: post: tags: [Conversas] operationId: replyToConversation summary: Responder conversa description: | Envia texto dentro da janela de 24h de uma conversa. Com a janela expirada, responde `400` com `{ "error": "..." }`: envie um template para reabrir. Falha no envio aparece em `status: FAILED` e `reason`. Permissão: `ADMIN`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ConversationReplyRequest' example: conversationId: 3f1c2b4a-5d6e-4f70-8a9b-0c1d2e3f4a5b body: Seu pedido sai hoje. responses: '200': description: Resultado do envio content: application/json: schema: $ref: '#/components/schemas/ConversationReplyResponse' '400': description: 'Janela expirada ou erro de envio. Corpo `{ "error": "mensagem" }`.' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: Conversa não encontrada (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} /v1/conversations/{conversationId}/status: patch: tags: [Conversas] operationId: updateConversationStatus summary: Alterar status da conversa description: 'Não apaga histórico. Permissão: `ADMIN`.' parameters: - in: path name: conversationId required: true schema: { type: string, format: uuid } requestBody: required: true content: application/json: schema: type: object required: [status] properties: status: type: string enum: [OPEN, CLOSED, ARCHIVED] responses: '200': description: Status aplicado content: application/json: schema: type: object properties: id: { type: string, format: uuid } status: { type: string } '400': description: 'Status inválido. Corpo `{ "error": "mensagem" }`.' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: Conversa não encontrada (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} /v1/conversations/window-status: post: tags: [Conversas] operationId: getWindowStatus summary: Consultar janela de 24h description: | Até 200 telefones por chamada; o excedente é ignorado e `truncated` volta `true`. Mande só dígitos com DDI, com ou sem `+` (`+5511999999999` ou `5511999999999`). A rota não normaliza como o envio: com `whatsapp:`, espaços, parênteses ou hífens o telefone não casa com a conversa e a janela volta como fechada. A busca tolera o `+` e o nono dígito de celular brasileiro. Permissão: `ADMIN`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WindowStatusRequest' example: phones: - '5511999999999' responses: '200': description: Situação por telefone content: application/json: schema: $ref: '#/components/schemas/WindowStatusResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- TEMPLATES ---------------------- /v1/templates: post: tags: [Templates] operationId: createTemplate summary: Criar template description: | Cria o template e envia para aprovação da Meta. `providerStatus` nasce `PENDING`; o resultado chega pelo webhook `template.updated`. - O nome é único por conta WhatsApp Business. Repetido: `409 CONFLICT`. - Em `AUTHENTICATION` o conteúdo é fixo: a Arara envia à Meta o texto `{{1}} é o seu código de verificação` com um botão de copiar código, em `pt_BR`. `body`, `buttons` e `language` enviados não mudam o template aprovado. No envio, passe o código como única variável. - Exige conta ativa (`403 ACCOUNT_NOT_ACTIVATED`). - Os planos atuais não têm limite de templates. Contas no plano Free legado têm teto de 3 templates aprovados e recebem `403 PLAN_LIMIT_REACHED`. Permissão: `TEMPLATES_WRITE`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateTemplateRequest' examples: simples: summary: Texto com variáveis value: name: pedido_confirmado category: UTILITY language: pt_BR body: 'Oi {{1}}, seu pedido {{2}} foi confirmado. Total: R$ {{3}}.' footer: Acompanhe pelo nosso site samples: { '1': João, '2': '12345', '3': '147,90' } botoes: summary: Com botões value: name: pedido_enviado category: UTILITY body: 'Oi {{1}}, seu pedido saiu para entrega.' samples: { '1': João } buttons: - {type: QUICK_REPLY, text: Confirmar} - {type: URL, text: Ver pedido, url: 'https://meusite.com/p/{{1}}'} carrossel: summary: Carrossel value: name: produtos_destaque category: MARKETING body: 'Confira nossas ofertas da semana:' carouselCards: - mediaUrl: https://meusite.com/produto-1.jpg body: Camiseta básica por R$ 79 buttons: - {type: URL, text: Comprar, url: 'https://meusite.com/p/1'} - mediaUrl: https://meusite.com/produto-2.jpg body: Tênis de corrida por R$ 299 buttons: - {type: URL, text: Comprar, url: 'https://meusite.com/p/2'} autenticacao: summary: Código de verificação value: name: otp_login category: AUTHENTICATION body: Código de verificação formulario: summary: Botão de formulário value: name: captar_contato category: MARKETING body: Oi, conta para a gente o que você procura? buttons: - {type: FLOW, text: Responder, flowId: 0b7e6a52-3c1d-4f7a-9a55-2f4f6f1f9c10} responses: '201': description: Template criado content: application/json: schema: $ref: '#/components/schemas/TemplateResponse' '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': description: | - Autenticação (corpo padrão do Spring): header ausente ou chave sem `TEMPLATES_WRITE`. - `ACCOUNT_NOT_ACTIVATED`: conta ainda não ativada. - `PLAN_LIMIT_REACHED`: só no plano Free legado, que tem teto de 3 templates aprovados. Os planos atuais não têm limite. content: application/json: schema: anyOf: - $ref: '#/components/schemas/Error' - $ref: '#/components/schemas/SpringError' example: error: code: PLAN_LIMIT_REACHED message: Limite de templates do plano atingido. details: { feature: TEMPLATES, current: 3, max: 3, currentPlan: FREE, upgradeTo: ESSENTIAL } '409': description: '`CONFLICT`: já existe um template com esse nome na mesma conta WhatsApp Business, ou o conteúdo foi bloqueado pela moderação da Arara.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: CONFLICT message: Já existe um template com o nome 'pedido_confirmado'. details: {} '422': description: '`UNPROCESSABLE_ENTITY`: categoria inválida, variável no começo ou no fim do corpo, botões ou carrossel fora dos limites, exemplos faltando.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: UNPROCESSABLE_ENTITY message: O corpo não pode começar nem terminar com uma variável. details: {} '429': {$ref: '#/components/responses/RateLimited'} get: tags: [Templates] operationId: listTemplates summary: Listar templates description: | Templates da organização, em `{ data, pagination }`. `availableForSending: false` indica template aprovado em um número que a organização não usa mais. A Meta aprova por conta WhatsApp Business: recrie o template no número ativo. Permissão: `READ`. parameters: - in: query name: name required: false schema: { type: string } description: Nome exato. Devolve no máximo 1 item. - in: query name: status required: false schema: type: string example: APPROVED description: Filtra por `providerStatus` (`APPROVED`, `PENDING`, `REJECTED`, `PAUSED`, `DISABLED`), sem diferenciar maiúsculas. - in: query name: page required: false schema: { type: integer, minimum: 0, default: 0 } description: Página, começando em `0`. - in: query name: size required: false schema: { type: integer, minimum: 1, maximum: 200, default: 50 } description: Itens por página. Máximo `200`. - $ref: '#/components/parameters/WabaIdHeader' responses: '200': description: Página de templates content: application/json: schema: $ref: '#/components/schemas/TemplatePage' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/templates/{id}: get: tags: [Templates] operationId: getTemplate summary: Consultar template description: | `id` é UUID. Passar o nome no lugar do ID responde `400 INVALID_PATH_PARAM`. Para buscar por nome, use `GET /v1/templates?name=`. Permissão: `READ`. parameters: - $ref: '#/components/parameters/TemplateId' responses: '200': description: Template content: application/json: schema: $ref: '#/components/schemas/TemplateResponse' '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '429': {$ref: '#/components/responses/RateLimited'} delete: tags: [Templates] operationId: deleteTemplate summary: Apagar template description: | Irreversível. Apaga primeiro na Meta e depois na Arara; se a Meta recusar, nada é apagado. O histórico de envios continua, mas o template sai da listagem e não pode mais ser enviado. Permissão: `TEMPLATES_WRITE`. parameters: - $ref: '#/components/parameters/TemplateId' responses: '204': description: Apagado '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '429': {$ref: '#/components/responses/RateLimited'} /v1/templates/{id}/status: get: tags: [Templates] operationId: getTemplateStatus summary: Consultar status de aprovação description: | Devolve o status que a Arara recebeu do provedor, sem nova consulta à Meta. Para saber da mudança sem consultar em laço, escute o webhook `template.updated`. Permissão: `READ`. 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'} '429': {$ref: '#/components/responses/RateLimited'} /v1/templates/{id}/analytics: get: tags: [Templates] operationId: getTemplateAnalytics summary: Consultar métricas do template description: | Enviadas, entregues, lidas e falhas do template no período. `sent` soma enviadas, entregues e lidas. As taxas vêm como **string** com 1 casa decimal, calculadas sobre `sent + failed`. Permissão: `READ`. parameters: - $ref: '#/components/parameters/TemplateId' - $ref: '#/components/parameters/AnalyticsPeriod' responses: '200': description: Métricas content: application/json: schema: $ref: '#/components/schemas/TemplateAnalyticsResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '429': {$ref: '#/components/responses/RateLimited'} /v1/templates/analytics: get: tags: [Templates] operationId: getTemplatesAnalytics summary: Listar métricas dos templates description: | Um item por nome de template enviado no período, com os mesmos campos da métrica individual (sem `templateId`). Permissão: `READ`. parameters: - $ref: '#/components/parameters/AnalyticsPeriod' responses: '200': description: Métricas por template content: application/json: schema: type: array items: $ref: '#/components/schemas/TemplateAnalyticsBatchItem' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/templates/library: get: tags: [Templates] operationId: listLibraryTemplates summary: Listar biblioteca de templates description: | Templates `UTILITY` que a Meta já aprovou. Exige número conectado (`422 GUPSHUP_APP_NOT_READY` sem ele). Permissão: `READ`. parameters: - in: query name: languageCode required: false schema: { type: string, default: pt_BR } description: Idioma dos templates da biblioteca. responses: '200': description: Templates da biblioteca content: application/json: schema: type: array items: $ref: '#/components/schemas/LibraryTemplateResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '422': description: '`GUPSHUP_APP_NOT_READY`: nenhum número conectado.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: GUPSHUP_APP_NOT_READY message: Conecte um número antes de usar a biblioteca. details: {} '429': {$ref: '#/components/responses/RateLimited'} post: tags: [Templates] operationId: createTemplateFromLibrary summary: Criar template da biblioteca description: | Cria um template a partir de um item da biblioteca da Meta. Nasce aprovado, sem esperar revisão. Exige número conectado (`422 GUPSHUP_APP_NOT_READY`). Permissão: `TEMPLATES_WRITE`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateLibraryTemplateRequest' example: name: confirmacao_pedido libraryTemplateName: order_confirmation_1 languageCode: pt_BR responses: '201': description: Template criado content: application/json: schema: $ref: '#/components/schemas/TemplateResponse' '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '409': description: '`CONFLICT`: já existe um template com esse nome na mesma conta WhatsApp Business.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: CONFLICT message: Já existe um template com o nome 'confirmacao_pedido'. details: {} '422': description: '`GUPSHUP_APP_NOT_READY`: nenhum número conectado.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: GUPSHUP_APP_NOT_READY message: Conecte um número antes de usar a biblioteca. details: {} '429': {$ref: '#/components/responses/RateLimited'} /v1/templates/sync: post: tags: [Templates] operationId: syncTemplates summary: Sincronizar templates description: | Importa templates criados direto no gerenciador da Meta e corrige status e categoria divergentes. Sem número conectado responde `422` `GUPSHUP_APP_NOT_READY`. Permissão: `TEMPLATES_WRITE`. responses: '200': description: Resultado da sincronização content: application/json: schema: type: object required: [imported, updated, unchanged, skipped] properties: imported: type: array items: { type: string } updated: type: array items: { type: string } unchanged: { type: integer } skipped: type: array items: { type: string } example: imported: - promo_setembro updated: - pedido_confirmado unchanged: 12 skipped: [] '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '422': {$ref: '#/components/responses/UnprocessableEntity'} '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- CAMPANHAS ---------------------- /v1/campaigns: post: tags: [Campanhas] operationId: createCampaign summary: Criar campanha description: | Cria a campanha, valida template e variáveis, reserva a cota do plano e **debita o custo total da carteira** antes de enfileirar. Os envios rodam em segundo plano. Contatos bloqueados no envio (opt-out, número inválido, limite de frequência) são estornados um a um. - `Idempotency-Key` é **obrigatório**. Repetir a chave devolve a campanha já criada, sem cobrar de novo. A unicidade da chave é global na Arara, não por organização: derive do identificador da campanha no seu sistema, com um prefixo só seu (ex. `acme-campanha-8842`). - `scheduledAt` agenda o envio, entre 5 minutos e 30 dias à frente. A campanha nasce `SCHEDULED`, já cobrada. Fora da janela: `400 CAMPAIGN_SCHEDULE_TOO_SOON` ou `400 CAMPAIGN_SCHEDULE_TOO_FAR`. - `abTest` cria o teste A/B junto com a campanha. `templateName` é a variante A. Pré-requisitos: número dedicado conectado (`403 NO_DEDICATED_NUMBER`), saldo para o total (`402 INSUFFICIENT_FUNDS`) e cota do plano (`403 PLAN_LIMIT_REACHED`). Permissão: `CAMPAIGNS_SEND`. parameters: - in: header name: Idempotency-Key required: true schema: { type: string, maxLength: 255 } description: Identificador único da campanha. Derive do identificador da campanha no seu sistema (ex. `acme-campanha-8842`) e reuse a mesma chave nas retentativas. A unicidade é global na Arara. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CampaignRequest' examples: simples: summary: Envio imediato value: name: Black Friday 2026 templateName: bf_oferta_geral contacts: - {to: '+5511999999999', variables: [João, '30']} - {to: '+5511888888888', variables: [Maria, '25']} agendada: summary: Agendada description: Troque `scheduledAt` por uma data entre 5 minutos e 30 dias à frente. value: name: Lembrete sexta templateName: lembrete_evento scheduledAt: '2027-01-15T12:00:00-03:00' contacts: - {to: '+5511999999999', variables: [João]} testeAB: summary: Com teste A/B value: name: Reativação templateName: reativacao_copy_a contacts: - {to: '+5511999999999', variables: [João]} abTest: variantBTemplateName: reativacao_copy_b metric: CLICKED samplePct: 20 splitPct: 50 decisionWindowMinutes: 240 autopilot: true responses: '200': description: Campanha criada (ou existente, se a chave já foi usada) content: application/json: schema: $ref: '#/components/schemas/CampaignResponse' '400': description: '`VALIDATION_ERROR` (`name`, `templateName` ou `contacts` ausente ou vazio), `CAMPAIGN_SCHEDULE_TOO_SOON` ou `CAMPAIGN_SCHEDULE_TOO_FAR` (`scheduledAt` fora da janela de 5 minutos a 30 dias).' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: CAMPAIGN_SCHEDULE_TOO_SOON message: Agende para pelo menos 5 minutos à frente. details: {} '401': {$ref: '#/components/responses/Unauthorized'} '402': {$ref: '#/components/responses/PaymentRequired'} '403': {$ref: '#/components/responses/Forbidden'} '422': description: '`UNPROCESSABLE_ENTITY` ou `ararahq-*`: template não encontrado, variáveis incompatíveis ou `sender` inválido.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: UNPROCESSABLE_ENTITY message: Template não encontrado. details: {} '429': {$ref: '#/components/responses/TooManyRequests'} '500': description: 'Requisição sem `Idempotency-Key` responde hoje `500 INTERNAL_SERVER_ERROR`, e não `400`.' content: application/json: schema: $ref: '#/components/schemas/Error' get: tags: [Campanhas] operationId: listCampaigns summary: Listar campanhas description: | Envelope `{ content, totalPages, totalElements }` (não segue o padrão `{ data, pagination }`). Permissão: `READ`. parameters: - in: query name: page required: false schema: { type: integer, minimum: 0, default: 0 } description: Página, começando em `0`. - in: query name: size required: false schema: { type: integer, minimum: 1, maximum: 100, default: 20 } description: Itens por página, de 1 a 100. - in: query name: status required: false schema: $ref: '#/components/schemas/CampaignStatus' description: Filtra por status da campanha. - $ref: '#/components/parameters/WabaIdHeader' responses: '200': description: Página de campanhas content: application/json: schema: $ref: '#/components/schemas/CampaignListResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/campaigns/estimate: get: tags: [Campanhas] operationId: estimateCampaign summary: Estimar custo por quantidade description: | Usa o preço de referência do mercado local. Para campanha com números de vários países, use o `POST`. `templateName` aceita nome ou ID. Permissão: `READ`. parameters: - in: query name: templateName required: true schema: { type: string } description: Nome ou ID do template. - in: query name: count required: true schema: { type: integer, minimum: 1, maximum: 1000000 } description: Quantidade de destinatários, de 1 a 1.000.000. responses: '200': description: Estimativa content: application/json: schema: $ref: '#/components/schemas/CampaignEstimateResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '422': {$ref: '#/components/responses/UnprocessableEntity'} '429': {$ref: '#/components/responses/RateLimited'} post: tags: [Campanhas] operationId: estimateCampaignByPhones summary: Estimar custo por lista de telefones description: | Calcula o preço por país de destino e devolve a quebra em `markets`. Não cria nada nem debita. Permissão: `CAMPAIGNS_SEND`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CampaignEstimateRequest' example: templateName: bf_oferta_geral phones: - '+5511999999999' - '+5511888888888' responses: '200': description: Estimativa content: application/json: schema: $ref: '#/components/schemas/CampaignEstimateResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '422': {$ref: '#/components/responses/UnprocessableEntity'} '429': {$ref: '#/components/responses/RateLimited'} /v1/campaigns/{id}: get: tags: [Campanhas] operationId: getCampaign summary: Consultar campanha description: | Campanha com as métricas agregadas. `clickedCount` e `convertedCount` dependem de Smart Links. Para acompanhar mensagem a mensagem, use o webhook `message.status_updated`; o fim da campanha gera o evento `campaign.completed`. Permissão: `READ`. parameters: - $ref: '#/components/parameters/CampaignId' responses: '200': description: Detalhe content: application/json: schema: $ref: '#/components/schemas/CampaignDetailResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '429': {$ref: '#/components/responses/RateLimited'} /v1/campaigns/{id}/cancel: post: tags: [Campanhas] operationId: cancelCampaign summary: Cancelar campanha description: | Marca `CANCELED` e estorna na carteira os contatos que ainda não viraram mensagem. Mensagens já enfileiradas seguem. Campanha em `COMPLETED`, `COMPLETED_WITH_ERRORS`, `CANCELED` ou `FAILED` responde `422`. Permissão: `CAMPAIGNS_SEND`. parameters: - $ref: '#/components/parameters/CampaignId' responses: '204': description: Cancelada '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '422': {$ref: '#/components/responses/UnprocessableEntity'} '429': {$ref: '#/components/responses/RateLimited'} /v1/campaigns/{campaignId}/ab-test: post: tags: [Campanhas] operationId: createCampaignAbTest summary: Adicionar teste A/B description: | Prefira o campo `abTest` no `POST /v1/campaigns`: lá o teste nasce junto com a campanha. Criado depois, parte dos contatos pode já ter sido enviada sem o teste. Nesta rota `variantATemplateName` é obrigatório e o padrão de `autopilot` é `false`. Permissão: `CAMPAIGNS_SEND`. parameters: - $ref: '#/components/parameters/CampaignIdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAbTestRequest' responses: '201': description: Teste criado content: application/json: schema: $ref: '#/components/schemas/AbTestResponse' '400': description: '`INVALID_AB_TEST` (métrica ou split inválido).' content: application/json: schema: $ref: '#/components/schemas/Error' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: Campanha não encontrada (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} get: tags: [Campanhas] operationId: getCampaignAbTest summary: Consultar teste A/B description: | Estado do teste: vencedora, quando e por que foi decidida, e as métricas por variante. Permissão: `READ`. parameters: - $ref: '#/components/parameters/CampaignIdPath' responses: '200': description: Teste A/B content: application/json: schema: $ref: '#/components/schemas/AbTestResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: Campanha ou teste não encontrado (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} /v1/campaigns/{campaignId}/ab-test/winner: post: tags: [Campanhas] operationId: forceCampaignAbTestWinner summary: Definir variante vencedora description: | Define o vencedor (`decidedReason = FORCED_MANUAL`) e libera os contatos que aguardavam o resultado. Permissão: `CAMPAIGNS_SEND`. parameters: - $ref: '#/components/parameters/CampaignIdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ForceWinnerRequest' example: winner: A responses: '200': description: Vencedor aplicado content: application/json: schema: $ref: '#/components/schemas/AbTestResponse' '400': description: '`INVALID_WINNER`.' content: application/json: schema: $ref: '#/components/schemas/Error' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: Campanha ou teste não encontrado (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- CONTATOS ---------------------- /v1/contacts: get: tags: [Contatos] operationId: listContacts summary: Listar contatos description: | Contatos da organização, no envelope `{ contacts, total, page, size, totalPages }` (não segue o padrão `{ data, pagination }`). Exige plano com contatos (`403 PLAN_FEATURE_LOCKED`). Permissão: `ADMIN`. parameters: - in: query name: q required: false schema: { type: string } description: Busca por nome, telefone ou email. - in: query name: lifecycle required: false schema: type: string enum: - ALL - NEW - ENGAGED - SILENT - DORMANT - OPTED_OUT description: Estágio do contato. `ALL` (ou omitido) traz todos. - in: query name: page required: false schema: { type: integer, minimum: 0, default: 0 } description: Página, começando em `0`. - in: query name: size required: false schema: { type: integer, minimum: 1, maximum: 200, default: 20 } description: Itens por página. Máximo `200`. - $ref: '#/components/parameters/WabaIdHeader' responses: '200': description: Página de contatos content: application/json: schema: $ref: '#/components/schemas/ContactsListResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/contacts/batch: post: tags: [Contatos] operationId: importContactsBatch summary: Importar contatos em lote description: | Cria ou atualiza até 5.000 contatos por chamada. O corpo é um **array**. O telefone é a chave: - Telefone novo: cria (`created`). - Telefone existente com dados diferentes: atualiza (`updated`). - Telefone existente com dados iguais: `skipped`, sem erro. - Linha inválida: `skipped` e entra em `errors` com o índice e o motivo. No lote, `email` e `attributes` **substituem** o que está gravado: enviar um contato existente sem `email` apaga o email dele. Para mudar só um campo, use `PATCH /v1/contacts/{phone}`. `importId` identifica a chamada nos logs; não há rota para consultá-lo. Permissão: `CONTACTS_WRITE`. parameters: - $ref: '#/components/parameters/WabaIdHeader' requestBody: required: true content: application/json: schema: type: array maxItems: 5000 items: $ref: '#/components/schemas/ContactRequest' example: - name: João Silva phone: '+5511999999999' email: joao@exemplo.com attributes: { plano: pro } - name: Maria Santos phone: '+5511888888888' responses: '200': description: Resultado da importação content: application/json: schema: $ref: '#/components/schemas/ContactsBatchResponse' '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '422': description: '`UNPROCESSABLE_ENTITY`: lote vazio ou acima de 5.000 itens.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: UNPROCESSABLE_ENTITY message: O lote aceita de 1 a 5.000 contatos. details: {} '429': {$ref: '#/components/responses/RateLimited'} /v1/contacts/stats: get: tags: [Contatos] operationId: getContactsStats summary: Consultar estatísticas de contatos description: 'Total de contatos e a contagem por estágio (`NEW`, `ENGAGED`, `SILENT`, `DORMANT`, `OPTED_OUT`). Permissão: `ADMIN`.' responses: '200': description: Contagens content: application/json: schema: $ref: '#/components/schemas/ContactsStatsResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/contacts/{phone}: get: tags: [Contatos] operationId: getContactByPhone summary: Consultar contato description: | Telefone E.164 com `+`. Contato inexistente responde hoje `422` `UNPROCESSABLE_ENTITY`, não `404`. Permissão: `ADMIN`. parameters: - $ref: '#/components/parameters/PhonePath' responses: '200': description: Contato content: application/json: schema: $ref: '#/components/schemas/ContactResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '422': description: '`UNPROCESSABLE_ENTITY`: contato não encontrado (hoje não responde `404`) ou telefone fora do E.164.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: UNPROCESSABLE_ENTITY message: Contato não encontrado. details: {} '429': {$ref: '#/components/responses/RateLimited'} patch: tags: [Contatos] operationId: updateContact summary: Atualizar contato description: 'Atualiza só os campos enviados. Campo omitido não muda. Permissão: `CONTACTS_WRITE`.' parameters: - $ref: '#/components/parameters/PhonePath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContactPatchRequest' example: name: João S. tags: - vip - black-friday responses: '200': description: Contato atualizado content: application/json: schema: $ref: '#/components/schemas/ContactResponse' '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '422': {$ref: '#/components/responses/UnprocessableEntity'} '429': {$ref: '#/components/responses/RateLimited'} /v1/contacts/{phone}/messages: get: tags: [Contatos] operationId: getContactMessages summary: Listar mensagens do contato description: 'Últimas mensagens trocadas com o telefone, da mais recente para a mais antiga. Permissão: `ADMIN`.' parameters: - $ref: '#/components/parameters/PhonePath' - in: query name: limit required: false schema: { type: integer, minimum: 1, maximum: 100, default: 30 } description: Quantidade de mensagens, até 100. responses: '200': description: Mensagens content: application/json: schema: $ref: '#/components/schemas/ContactMessagesResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '422': {$ref: '#/components/responses/UnprocessableEntity'} '429': {$ref: '#/components/responses/RateLimited'} /v1/contacts/{phone}/lgpd/export: get: tags: [Contatos] operationId: exportContactLgpd summary: Exportar dados do titular description: 'Contato, conversa e até 1.000 mensagens. Permissão: `ADMIN`.' parameters: - $ref: '#/components/parameters/PhonePath' responses: '200': description: Dados do titular content: application/json: schema: $ref: '#/components/schemas/LgpdExport' '400': {$ref: '#/components/responses/InvalidPhone'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/contacts/{phone}/lgpd: delete: tags: [Contatos] operationId: deleteContactLgpd summary: Apagar dados do titular description: | Irreversível. Mascara o conteúdo das mensagens e da conversa, apaga o contato e registra opt-out. Permissão: `CONTACTS_WRITE`. parameters: - $ref: '#/components/parameters/PhonePath' - in: query name: reason required: false schema: { type: string, maxLength: 80 } description: Motivo do pedido, para o registro de opt-out. Até 80 caracteres. responses: '200': description: Resultado da remoção content: application/json: schema: $ref: '#/components/schemas/LgpdDeletionResult' '400': {$ref: '#/components/responses/InvalidPhone'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- OPT-OUT ---------------------- /v1/opt-outs: get: tags: [Opt-out] operationId: listOptOuts summary: Listar opt-outs description: 'Todos os opt-outs da organização, em `{ items, total }`, sem paginação. Permissão: `ADMIN`.' responses: '200': description: Opt-outs content: application/json: schema: type: object required: [items, total] properties: items: type: array items: $ref: '#/components/schemas/OptOutItem' total: { type: integer } '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} post: tags: [Opt-out] operationId: createOptOut summary: Registrar opt-out description: | Registra que o telefone não quer mais receber mensagens da organização. Registrar o mesmo telefone de novo devolve o registro existente. Atenção: o envio hoje só é bloqueado por opt-out feito por palavra-chave recebida no WhatsApp (ex. "SAIR", "PARAR", "STOP"). O opt-out registrado por esta rota ou pela remoção LGPD ainda **não** é consultado no envio de mensagens e campanhas. Filtre a lista do seu lado antes de enviar. Permissão: `ADMIN`. requestBody: required: true content: application/json: schema: type: object required: [phone] properties: phone: type: string example: '+5511999999999' description: Telefone E.164 com `+`. reason: type: string maxLength: 80 description: Motivo. Cortado em 80 caracteres. example: phone: '+5511999999999' reason: pediu por email responses: '201': description: Opt-out registrado content: application/json: schema: $ref: '#/components/schemas/OptOutItem' '400': {$ref: '#/components/responses/InvalidPhone'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/opt-outs/{phone}: get: tags: [Opt-out] operationId: checkOptOut summary: Consultar opt-out description: 'Diz se o telefone está em opt-out. Telefone fora do E.164 responde `400 INVALID_PHONE`. Permissão: `ADMIN`.' parameters: - $ref: '#/components/parameters/PhonePath' responses: '200': description: Situação content: application/json: schema: type: object required: [phone, channel, optedOut] properties: phone: { type: string } channel: type: string example: WHATSAPP optedOut: { type: boolean } example: phone: '+5511999999999' channel: WHATSAPP optedOut: true '400': {$ref: '#/components/responses/InvalidPhone'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} delete: tags: [Opt-out] operationId: deleteOptOut summary: Remover opt-out description: 'Remove o opt-out: o telefone volta a poder receber (opt-in). Permissão: `ADMIN`.' parameters: - $ref: '#/components/parameters/PhonePath' responses: '204': description: Removido '400': {$ref: '#/components/responses/InvalidPhone'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- FORMULÁRIOS ---------------------- /v1/flows: get: tags: [Formulários] operationId: listFlows summary: Listar formulários description: | Formulários da organização, do mais recente para o mais antigo, em `{ data, pagination }`. O `id` de cada item é o `flowId` usado no envio. Permissão: `READ`. parameters: - $ref: '#/components/parameters/Page' - in: query name: size required: false schema: { type: integer, minimum: 1, maximum: 200, default: 20 } description: Itens por página. responses: '200': description: Página de formulários content: application/json: schema: $ref: '#/components/schemas/FlowPage' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} post: tags: [Formulários] operationId: createFlow summary: Criar formulário description: | Cria o formulário e já publica: quando a chamada volta `201` com `status: PUBLISHED`, ele está no ar e não pode mais ser editado. Se só a publicação falhar, o formulário fica em `DRAFT` com `publishError` preenchido; publique de novo em `POST /v1/flows/{id}/publish`. Não aceita `Idempotency-Key`. O nome é único por número, então repetir um `POST` que já passou responde `409 FLOW_NAME_TAKEN` em vez de duplicar. Disponível do plano Voo para cima. Permissão: `TEMPLATES_WRITE`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateFlowRequest' example: name: Captar contato category: LEAD_GENERATION title: Fale com a gente intro: Leva menos de um minuto. submitLabel: Enviar fields: - {label: Seu nome, type: TEXT, key: nome} - {label: Seu melhor e-mail, type: EMAIL, key: email} - label: O que você procura? type: SELECT key: interesse options: [Falar com vendas, Suporte, Parceria] - {label: Observações, type: LONG_TEXT, key: obs, required: false} responses: '201': description: Formulário criado content: application/json: schema: $ref: '#/components/schemas/Flow' '400': description: '`INVALID_FLOW`: pergunta em branco, opções de menos, chave inválida ou JSON recusado pela Meta. Também `INVALID_JSON`.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: {code: INVALID_FLOW, message: 'O formulário precisa de 1 a 10 perguntas.', details: {}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '409': description: '`FLOW_NAME_TAKEN` (nome já usado no número) ou `FLOW_NUMBER_REQUIRED` (nenhum número conectado).' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: {code: FLOW_NAME_TAKEN, message: "Já existe um formulário com o nome 'Captar contato'.", details: {}} '429': {$ref: '#/components/responses/RateLimited'} /v1/flows/{id}: get: tags: [Formulários] operationId: getFlow summary: Consultar formulário description: 'Devolve o formulário com a definição das perguntas e o total de respostas. Permissão: `READ`.' parameters: - $ref: '#/components/parameters/FlowId' responses: '200': description: Formulário content: application/json: schema: $ref: '#/components/schemas/Flow' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/FlowNotFound'} '429': {$ref: '#/components/responses/RateLimited'} delete: tags: [Formulários] operationId: deleteFlow summary: Apagar formulário description: | Só funciona em formulário que nunca foi publicado (`DRAFT`). Publicado responde `409 FLOW_NOT_DRAFT`: use `POST /v1/flows/{id}/deprecate`. Permissão: `TEMPLATES_WRITE`. parameters: - $ref: '#/components/parameters/FlowId' responses: '204': description: Apagado '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/FlowNotFound'} '409': {$ref: '#/components/responses/FlowStateConflict'} '429': {$ref: '#/components/responses/RateLimited'} /v1/flows/{id}/publish: post: tags: [Formulários] operationId: publishFlow summary: Publicar formulário description: | Publica um formulário que ficou em `DRAFT` porque a publicação falhou na criação. Formulário já publicado responde `409 FLOW_NOT_DRAFT`. Permissão: `TEMPLATES_WRITE`. parameters: - $ref: '#/components/parameters/FlowId' responses: '200': description: Formulário publicado content: application/json: schema: $ref: '#/components/schemas/Flow' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/FlowNotFound'} '409': {$ref: '#/components/responses/FlowStateConflict'} '429': {$ref: '#/components/responses/RateLimited'} /v1/flows/{id}/deprecate: post: tags: [Formulários] operationId: deprecateFlow summary: Desativar formulário description: | Marca o formulário como `DEPRECATED`. As respostas já recebidas ficam guardadas e o botão deixa de abrir para quem recebeu a mensagem antes. Só formulário publicado pode ser desativado (`409 FLOW_NOT_PUBLISHED`). Permissão: `TEMPLATES_WRITE`. parameters: - $ref: '#/components/parameters/FlowId' responses: '200': description: Formulário desativado content: application/json: schema: $ref: '#/components/schemas/Flow' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/FlowNotFound'} '409': {$ref: '#/components/responses/FlowStateConflict'} '429': {$ref: '#/components/responses/RateLimited'} /v1/flows/{id}/test: post: tags: [Formulários] operationId: testFlow summary: Enviar formulário de teste description: | Envia o formulário para o telefone do usuário dono da chave. É uma mensagem real, dentro da janela de 24h: exige formulário publicado e conversa aberta com esse telefone. Permissão: `MESSAGES_SEND`. parameters: - $ref: '#/components/parameters/FlowId' responses: '200': description: Teste enviado content: application/json: schema: type: object required: [sentTo] properties: sentTo: type: string description: Número que recebeu o teste, com DDI. example: sentTo: '5511999999999' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/FlowNotFound'} '409': description: '`FLOW_NOT_PUBLISHED`, `FLOW_TEST_NO_PHONE` (usuário sem telefone), `FLOW_TEST_WINDOW_CLOSED` (janela de 24h fechada) ou `FLOW_TEST_FAILED`.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: {code: FLOW_TEST_WINDOW_CLOSED, message: 'Mande uma mensagem para o seu número antes de testar.', details: {}} '429': {$ref: '#/components/responses/RateLimited'} /v1/flows/{id}/responses: get: tags: [Formulários] operationId: listFlowResponses summary: Listar respostas do formulário description: | Respostas recebidas, em `{ data, pagination }`. Para receber cada resposta em tempo real, use o webhook `message.flow_reply`. Permissão: `READ`. parameters: - $ref: '#/components/parameters/FlowId' - $ref: '#/components/parameters/Page' - in: query name: size required: false schema: { type: integer, minimum: 1, maximum: 200, default: 20 } description: Itens por página. responses: '200': description: Página de respostas content: application/json: schema: $ref: '#/components/schemas/FlowSubmissionPage' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/FlowNotFound'} '429': {$ref: '#/components/responses/RateLimited'} /v1/flows/{id}/responses.csv: get: tags: [Formulários] operationId: exportFlowResponsesCsv summary: Exportar respostas em CSV description: | Planilha com todas as respostas do formulário, em UTF-8 com BOM (abre com acentos certos no Excel). Permissão: `READ`. parameters: - $ref: '#/components/parameters/FlowId' responses: '200': description: Arquivo CSV, enviado como anexo. content: text/csv: schema: type: string format: binary '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/FlowNotFound'} '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- AUTOMAÇÕES ---------------------- /v1/automations: get: tags: [Automações] operationId: listAutomations summary: Listar automações description: | Todas as automações da organização, sem paginação. Exige plano com recuperação de receita. Permissão: `READ`. responses: '200': description: Automações content: application/json: schema: type: array items: $ref: '#/components/schemas/Automation' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} post: tags: [Automações] operationId: createAutomation summary: Criar automação description: | A automação nasce sempre desligada (`active: false`). Criar não conta no teto de automações ativas do plano; ligar conta. Ligue com `PUT /v1/automations/{id}/active`. Com `Idempotency-Key`, repetir a chamada devolve a automação já criada. Permissão: `AUTOMATIONS_MANAGE`. Essa permissão é opcional na chave: chaves antigas não têm. parameters: - $ref: '#/components/parameters/IdempotencyKeyOptional' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AutomationUpsertRequest' examples: carrinho: summary: Lembrete de carrinho abandonado value: name: Lembrete de carrinho trigger: cart.abandoned steps: - type: wait config: { minutes: 60 } - type: message config: templateId: 0b8f6c1e-7a52-4a1b-9a0e-3f2d6c9e1a44 variables: [nome] cobranca: summary: Cobrança com espera de pagamento value: name: Cobrança do plano mensal trigger: webhook steps: - type: message config: templateId: 0b8f6c1e-7a52-4a1b-9a0e-3f2d6c9e1a44 variables: [nome] - type: wait_payment config: { timeoutMinutes: 60 } branches: paid: - type: tag config: { tag: pagou } unpaid: - type: message config: templateId: 5d2a9b7c-1e44-4f0a-8c3b-6a7e2d1f9b10 variables: [nome] responses: '201': description: Automação criada content: application/json: schema: $ref: '#/components/schemas/Automation' '400': {$ref: '#/components/responses/AutomationInvalid'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/automations/catalog: get: tags: [Automações] operationId: getAutomationCatalog summary: Listar gatilhos e tipos de passo description: | Catálogo de valores aceitos em `trigger` e `steps[].type`. Útil para validar do seu lado antes de criar. Permissão: `READ`. responses: '200': description: Catálogo content: application/json: schema: $ref: '#/components/schemas/AutomationCatalog' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/automations/templates: get: tags: [Automações] operationId: listAutomationTemplates summary: Listar templates para automação description: | Só os templates aprovados, cada um com o preço de referência do envio. Use o `id` em `steps[].config.templateId`. Permissão: `READ`. responses: '200': description: Templates aprovados com preço content: application/json: schema: type: array items: $ref: '#/components/schemas/AutomationTemplateOption' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/automations/pricing: get: tags: [Automações] operationId: getAutomationPricing summary: Consultar preço da mensagem na janela description: | Preço de uma mensagem dentro da janela de 24h (texto, botões, lista) no plano da organização. Serve para somar o custo da automação antes de ligar. Permissão: `READ`. responses: '200': description: Preço content: application/json: schema: type: object required: [sessionUnitPriceBrl] properties: sessionUnitPriceBrl: type: number description: Preço em reais. '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/automations/{id}: get: tags: [Automações] operationId: getAutomation summary: Consultar automação description: | Mesmo objeto da listagem, com `stats` e, no gatilho `webhook`, o objeto `hook` com o link próprio da automação. Permissão: `READ`. parameters: - $ref: '#/components/parameters/AutomationId' responses: '200': description: Automação content: application/json: schema: $ref: '#/components/schemas/Automation' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/AutomationNotFound'} '429': {$ref: '#/components/responses/RateLimited'} put: tags: [Automações] operationId: updateAutomation summary: Atualizar automação description: | Substitui a automação inteira: nome, gatilho e a lista de passos. Não existe alteração parcial. Mande o objeto completo, como `GET /v1/automations/{id}` devolveu. Salvar uma automação ligada encerra as execuções que estão esperando resposta (`question`) ou pagamento (`wait_payment`), com `stoppedReason: "automação editada"`. Permissão: `AUTOMATIONS_MANAGE`. parameters: - $ref: '#/components/parameters/AutomationId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AutomationUpsertRequest' responses: '200': description: Automação atualizada content: application/json: schema: $ref: '#/components/schemas/Automation' '400': {$ref: '#/components/responses/AutomationInvalid'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/AutomationNotFound'} '429': {$ref: '#/components/responses/RateLimited'} delete: tags: [Automações] operationId: deleteAutomation summary: Apagar automação description: 'Apaga a automação e as execuções ligadas a ela. Permissão: `AUTOMATIONS_MANAGE`.' parameters: - $ref: '#/components/parameters/AutomationId' responses: '204': description: Apagada '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/AutomationNotFound'} '429': {$ref: '#/components/responses/RateLimited'} /v1/automations/{id}/active: put: tags: [Automações] operationId: setAutomationActive summary: Ligar ou desligar automação description: | O teto de automações ativas do plano é conferido aqui, não na criação, e ignora a própria automação. Automação sem passos não liga (`400 AUTOMATION_EMPTY`). Desligar não derruba quem já está no meio da sequência, só impede gente nova de entrar. Permissão: `AUTOMATIONS_MANAGE`. parameters: - $ref: '#/components/parameters/AutomationId' requestBody: required: true content: application/json: schema: type: object required: [active] properties: active: type: boolean description: '`true` liga, `false` desliga.' example: active: true responses: '200': description: Automação com o novo estado content: application/json: schema: $ref: '#/components/schemas/Automation' '400': description: '`AUTOMATION_EMPTY`: automação sem nenhum passo.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: {code: AUTOMATION_EMPTY, message: Adicione pelo menos um passo antes de ligar., details: {}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/AutomationNotFound'} '429': {$ref: '#/components/responses/RateLimited'} /v1/automations/{id}/runs: get: tags: [Automações] operationId: listAutomationRuns summary: Listar execuções da automação description: | Uma execução é uma pessoa atravessando a automação. Devolve `{ data, pagination }`. Permissão: `READ`. parameters: - $ref: '#/components/parameters/AutomationId' - $ref: '#/components/parameters/Page' - in: query name: size required: false schema: { type: integer, minimum: 1, default: 20 } description: Itens por página. responses: '200': description: Página de execuções content: application/json: schema: $ref: '#/components/schemas/AutomationRunPage' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/AutomationNotFound'} '429': {$ref: '#/components/responses/RateLimited'} /v1/automations/{id}/runs/{runId}/events: get: tags: [Automações] operationId: listAutomationRunEvents summary: Listar eventos de uma execução description: | O rastro de uma execução, passo a passo. Responde por que uma pessoa parou onde parou. Devolve `{ data, pagination }`. Permissão: `READ`. parameters: - $ref: '#/components/parameters/AutomationId' - in: path name: runId required: true schema: { type: string, format: uuid } description: ID da execução, como veio em `GET /v1/automations/{id}/runs`. - $ref: '#/components/parameters/Page' - in: query name: size required: false schema: { type: integer, minimum: 1, default: 50 } description: Itens por página. responses: '200': description: Página de eventos content: application/json: schema: $ref: '#/components/schemas/AutomationRunEventPage' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: '`AUTOMATION_NOT_FOUND` ou `AUTOMATION_RUN_NOT_FOUND`.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: {code: AUTOMATION_RUN_NOT_FOUND, message: Execução não encontrada., details: {}} '429': {$ref: '#/components/responses/RateLimited'} /v1/automations/{id}/hook/regenerate: post: tags: [Automações] operationId: regenerateAutomationHook summary: Gerar novo link da automação description: | Troca o token do link próprio da automação. O link antigo para de funcionar na hora. Só existe em automação com gatilho `webhook` (`409 HOOK_UNAVAILABLE_FOR_TRIGGER` nas demais). Permissão: `AUTOMATIONS_MANAGE`. parameters: - $ref: '#/components/parameters/AutomationId' responses: '200': description: Link novo content: application/json: schema: $ref: '#/components/schemas/AutomationHook' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/AutomationNotFound'} '409': {$ref: '#/components/responses/HookUnavailable'} '429': {$ref: '#/components/responses/RateLimited'} /v1/automations/{id}/hook/signature: put: tags: [Automações] operationId: setAutomationHookSignature summary: Ligar ou desligar assinatura do link description: | Com a assinatura ligada, o link só aceita `POST` com o header `X-Arara-Signature` válido. O segredo vem **uma única vez**, na resposta de quem ligou. Guarde na hora. Permissão: `AUTOMATIONS_MANAGE`. parameters: - $ref: '#/components/parameters/AutomationId' requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: '`true` liga a assinatura. Ausente ou `false` desliga.' example: enabled: true responses: '200': description: Estado da assinatura content: application/json: schema: $ref: '#/components/schemas/AutomationHookSignature' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/AutomationNotFound'} '409': {$ref: '#/components/responses/HookUnavailable'} '429': {$ref: '#/components/responses/RateLimited'} '503': description: '`HOOK_SIGNATURE_UNAVAILABLE`: a assinatura não pôde ser configurada agora. Tente de novo.' content: application/json: schema: $ref: '#/components/schemas/Error' /hooks/a/{token}: post: tags: [Automações] operationId: triggerAutomationHook summary: Acionar automação pelo link description: | Link próprio de uma automação com gatilho `webhook`. Não usa chave de API: o `token` na URL é a credencial. Trate o link como segredo. O mesmo link aceita dois corpos: - Com `phone`: inicia uma execução (`202`). - Com `referenceId` e `status` na raiz: atualiza o pagamento de uma cobrança já enviada (`200`), e a execução segue pelo ramo `paid` ou `unpaid` do passo `wait_payment`. Corpo de até 64 KB. Guia completo em [Link da automação e pagamento](/automacoes-webhook). security: [] parameters: - in: path name: token required: true schema: { type: string } description: Token do link (`ara_hook_...`), copiado da tela da automação ou do campo `hook.url`. - in: header name: X-Arara-Signature required: false schema: { type: string } description: Assinatura do corpo. Obrigatória quando a assinatura do link está ligada. - in: header name: Idempotency-Key required: false schema: { type: string } description: Evita iniciar a mesma execução duas vezes. Use o id do evento no seu sistema (ex. o número do pedido). requestBody: required: true content: application/json: schema: oneOf: - $ref: '#/components/schemas/HookStartRequest' - $ref: '#/components/schemas/HookPaymentUpdateRequest' examples: iniciar: summary: Iniciar com cobrança por Pix value: phone: '5511999999999' variables: { nome: Maria } charge: referenceId: pedido-1042 method: pix pixCode: 000201... pixKey: '39580525000189' pixKeyType: CNPJ merchantName: Loja Exemplo amountCents: 5990 description: Plano mensal pagamento: summary: Avisar que pagou value: referenceId: pedido-1042 status: paid responses: '200': description: Pagamento atualizado content: application/json: schema: $ref: '#/components/schemas/HookPaymentUpdated' example: referenceId: pedido-1042 status: paid cardUpdated: true runResumed: true '202': description: Execução iniciada content: application/json: schema: $ref: '#/components/schemas/HookStarted' example: runId: 7c1d2e90-4b3a-4c8e-9f21-5a6b7c8d9e01 status: started '400': description: '`HOOK_PAYLOAD_INVALID`, `INVALID_CHARGE` ou `INVALID_CHARGE_STATUS`.' content: application/json: schema: $ref: '#/components/schemas/Error' '401': description: '`HOOK_SIGNATURE_INVALID`: assinatura ausente ou errada com a assinatura ligada.' content: application/json: schema: $ref: '#/components/schemas/Error' '403': description: '`HOOK_SETTLEMENT_SIGNATURE_REQUIRED`: a baixa de pagamento exige a assinatura ligada.' content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: '`HOOK_NOT_FOUND` (token inexistente ou trocado) ou `CHARGE_NOT_FOUND`.' content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: '`AUTOMATION_INACTIVE` (automação desligada), `CHARGE_AMOUNT_MISMATCH` ou `CHARGE_ALREADY_SETTLED`.' content: application/json: schema: $ref: '#/components/schemas/Error' '413': description: '`HOOK_PAYLOAD_TOO_LARGE`: corpo acima de 64 KB.' content: application/json: schema: $ref: '#/components/schemas/Error' '429': description: Limite de chamadas do link. Respeite `Retry-After`. headers: Retry-After: schema: { type: integer } description: Segundos a aguardar antes de tentar de novo. content: application/json: schema: $ref: '#/components/schemas/Error' # ---------------------- SMART LINKS ---------------------- /v1/smart-links/whatsapp: post: tags: [Smart Links] operationId: createSmartLink summary: Criar smart link description: 'Cria um link `wa.me` rastreável, com QR code, que abre a conversa com o seu número. Permissão: `ADMIN`.' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateSmartLinkRequest' responses: '200': description: Smart link criado content: application/json: schema: $ref: '#/components/schemas/SmartLinkResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} get: tags: [Smart Links] operationId: listSmartLinks summary: Listar smart links description: 'Paginado em `{ data, pagination }`. Permissão: `ADMIN`.' parameters: - in: query name: page required: false schema: { type: integer, minimum: 0, default: 0 } - in: query name: size required: false schema: { type: integer, minimum: 1, maximum: 200, default: 50 } responses: '200': description: Página de smart links content: application/json: schema: $ref: '#/components/schemas/SmartLinkPage' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/smart-links/whatsapp/{id}: put: tags: [Smart Links] operationId: updateSmartLink summary: Atualizar smart link description: 'O telefone não muda. Permissão: `ADMIN`.' parameters: - in: path name: id required: true schema: { type: string, format: uuid } requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateSmartLinkRequest' responses: '200': description: Smart link atualizado content: application/json: schema: $ref: '#/components/schemas/SmartLinkResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/smart-links/whatsapp/{id}/stats: get: tags: [Smart Links] operationId: getSmartLinkStats summary: Consultar cliques do smart link description: 'Total de cliques do smart link. Permissão: `ADMIN`.' parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: '200': description: Cliques content: application/json: schema: type: object required: [linkId, totalClicks] properties: linkId: { type: string, format: uuid } totalClicks: { type: integer, format: int64 } '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: Smart link não encontrado (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- NÚMEROS ---------------------- /v1/organizations/me/numbers: get: tags: [Números] operationId: listNumbers summary: Listar números description: 'Números ativos da organização, com qualidade, limite diário (tier) e uso do plano. Permissão: `READ`.' responses: '200': description: Números e slot do plano content: application/json: schema: $ref: '#/components/schemas/NumbersResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/organizations/me/business-profile: get: tags: [Números] operationId: getBusinessProfile summary: Consultar perfil comercial description: 'Perfil comercial salvo na Arara e o estado da publicação no WhatsApp. Permissão: `ADMIN`.' responses: '200': description: Perfil content: application/json: schema: $ref: '#/components/schemas/BusinessProfileResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} patch: tags: [Números] operationId: updateBusinessProfile summary: Atualizar perfil comercial description: | Salva localmente e marca `metaSyncStatus = PENDING`. Para publicar no WhatsApp, chame `/sync`. Permissão: `ADMIN`. 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': description: '`INVALID_VERTICAL`.' content: application/json: schema: $ref: '#/components/schemas/Error' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/organizations/me/business-profile/sync: post: tags: [Números] operationId: syncBusinessProfile summary: Publicar perfil comercial description: 'Envia para o WhatsApp o perfil salvo em `PATCH /v1/organizations/me/business-profile`. Permissão: `ADMIN`.' responses: '200': description: Resultado da publicação content: application/json: schema: type: object additionalProperties: true '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: Perfil ainda não criado (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- CARTEIRA ---------------------- /dashboard/wallet/balance: get: tags: [Carteira] operationId: getWalletBalance summary: Consultar saldo description: 'Saldo da carteira da organização, em reais (decimal). Permissão: `ADMIN`.' responses: '200': description: Saldo content: application/json: schema: $ref: '#/components/schemas/WalletBalanceResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/wallet/transactions: get: tags: [Carteira] operationId: listWalletTransactions summary: Listar extrato description: 'Mais recentes primeiro. Permissão: `ADMIN`.' parameters: - in: query name: page required: false schema: { type: integer, minimum: 0, default: 0 } - in: query name: size required: false schema: { type: integer, minimum: 1, maximum: 100, default: 20 } responses: '200': description: Página do extrato content: application/json: schema: $ref: '#/components/schemas/WalletTransactionPage' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/wallet/auto-recharge: get: tags: [Carteira] operationId: getAutoRecharge summary: Consultar recarga automática description: 'Estado da recarga automática: ligada ou não, limite de saldo e valor da recarga. Permissão: `ADMIN`.' responses: '200': description: Configuração content: application/json: schema: $ref: '#/components/schemas/AutoRechargeSettings' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: Organização sem carteira (sem corpo). '429': {$ref: '#/components/responses/RateLimited'} patch: tags: [Carteira] operationId: updateAutoRecharge summary: Configurar recarga automática description: | Ativar exige cartão cadastrado (`409 CONFLICT` sem ele). Se o saldo já estiver no limite ao ativar, a cobrança acontece na hora. Permissão: `ADMIN`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateAutoRechargeRequest' responses: '200': description: Configuração atualizada content: application/json: schema: $ref: '#/components/schemas/AutoRechargeSettings' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/UnprocessableEntity'} '429': {$ref: '#/components/responses/RateLimited'} /v1/wallet/calculate: get: tags: [Carteira] operationId: calculateRecharge summary: Simular recarga description: | `amountCents` fora de 2.000 a 5.000.000 é ajustado ao limite mais próximo. Permissão: `ADMIN`. parameters: - in: query name: amountCents required: true schema: { type: integer } responses: '200': description: Simulação content: application/json: schema: $ref: '#/components/schemas/RechargeEstimate' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/pricing/current: get: tags: [Carteira] operationId: getCurrentPricing summary: Consultar preços description: | `categories` traz o preço de referência (BRL) no mercado local; o valor cobrado depende do país de destino. `markets` traz o custo Meta em USD. Permissão: `ADMIN`. parameters: - in: query name: market required: false schema: { type: string } description: Filtra `markets` por um mercado. responses: '200': description: Preços content: application/json: schema: $ref: '#/components/schemas/PricingResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- CONSULTA DE TELEFONE ---------------------- /v1/phone-lookup: post: tags: [Consulta de telefone] operationId: phoneLookup summary: Consultar telefone description: 'Cobra R$ 0,05 por consulta na carteira. Permissão: `ADMIN`.' requestBody: required: true content: application/json: schema: type: object required: [phoneNumber] properties: phoneNumber: { type: string } responses: '200': description: Resultado content: application/json: schema: $ref: '#/components/schemas/LookupResult' '401': {$ref: '#/components/responses/Unauthorized'} '402': {$ref: '#/components/responses/PaymentRequired'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/phone-lookup/batch: post: tags: [Consulta de telefone] operationId: phoneLookupBatch summary: Consultar telefones em lote description: | Processa em série. Ao faltar saldo, para de cobrar e marca as linhas restantes com `error = INSUFFICIENT_FUNDS`. Permissão: `ADMIN`. requestBody: required: true content: application/json: schema: type: object required: [phoneNumbers] properties: phoneNumbers: type: array items: { type: string } responses: '200': description: Resultados content: application/json: schema: type: object required: [results, totalCharged, insufficientFunds] properties: results: type: array items: $ref: '#/components/schemas/LookupResult' totalCharged: { type: number } insufficientFunds: { type: boolean } '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- RECUPERAÇÃO ---------------------- /v1/recovery/endpoint: get: tags: [Recuperação] operationId: getRecoveryEndpoint summary: Consultar endpoint de recuperação description: 'Devolve o `hmacSecret`: trate como segredo. Permissão: `ADMIN`.' responses: '200': description: Endpoint content: application/json: schema: $ref: '#/components/schemas/RecoveryEndpointResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} patch: tags: [Recuperação] operationId: patchRecoveryEndpoint summary: Ativar ou desativar endpoint description: 'Liga ou desliga o recebimento de eventos de recuperação. Permissão: `ADMIN`.' requestBody: required: true content: application/json: schema: type: object properties: active: { type: boolean } responses: '200': description: Endpoint content: application/json: schema: $ref: '#/components/schemas/RecoveryEndpointResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/recovery/endpoint/regenerate: post: tags: [Recuperação] operationId: regenerateRecoveryEndpoint summary: Gerar novo segredo description: 'Troca o segredo HMAC do endpoint. O segredo antigo para de valer na hora. Permissão: `ADMIN`.' responses: '200': description: Endpoint content: application/json: schema: $ref: '#/components/schemas/RecoveryEndpointResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/recovery/events: get: tags: [Recuperação] operationId: listRecoveryEvents summary: Listar eventos de recuperação description: 'Configuração de cada tipo de evento de recuperação (template, espera, ativo). Permissão: `ADMIN`.' responses: '200': description: Configurações content: application/json: schema: $ref: '#/components/schemas/RecoveryEventsListResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/recovery/events/{eventType}: put: tags: [Recuperação] operationId: upsertRecoveryEventConfig summary: Configurar evento de recuperação description: | Deixar ativa conta contra o limite de automações do plano (`PLAN_LIMIT_REACHED`). Permissão: `ADMIN`. 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: Configuração content: application/json: schema: $ref: '#/components/schemas/RecoveryEventConfigResponse' '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/recovery/events/{eventType}/test: post: tags: [Recuperação] operationId: testRecoveryEvent summary: Testar evento de recuperação description: | Envia mensagem real para o telefone do usuário dono da chave. Permissão: `ADMIN`. parameters: - in: path name: eventType required: true schema: { type: string } responses: '200': description: Resultado content: application/json: schema: $ref: '#/components/schemas/RecoveryIngestResult' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /webhook/cart: post: tags: [Recuperação] operationId: cartAbandonedWebhook deprecated: true summary: Enviar carrinho abandonado (legado) description: | Rota anterior ao endpoint de recuperação. Para integrações novas use `/v1/recovery/*`. Permissão: `WEBHOOKS_MANAGE`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CartAbandonedWebhookRequest' responses: '202': description: Aceito content: application/json: schema: $ref: '#/components/schemas/CartAbandonedWebhookResponse' '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} # ---------------------- CONTA ---------------------- /auth/me: get: tags: [Conta] operationId: getCurrentUser summary: Consultar usuário da chave description: | Devolve o usuário dono da chave. Serve para conferir, no início da integração, se a chave configurada é a esperada. Permissão: `ADMIN`. responses: '200': description: Usuário content: application/json: schema: $ref: '#/components/schemas/UserResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /auth/me/organizations: get: tags: [Conta] operationId: getMyOrganizations summary: Listar organizações da chave description: | Organizações do usuário dono da chave, na ordem em que ele entrou. A chave sempre age sobre a organização principal desse usuário. Permissão: `ADMIN`. responses: '200': description: Organizações content: application/json: schema: $ref: '#/components/schemas/OrganizationListResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /v1/organizations/me/plan: get: tags: [Conta] operationId: getOrganizationPlan summary: Consultar plano description: 'Valores em centavos. Permissão: `ADMIN`.' responses: '200': description: Plano content: application/json: schema: $ref: '#/components/schemas/PlanInfoResponse' '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '429': {$ref: '#/components/responses/RateLimited'} /health: get: tags: [Conta] operationId: health summary: Verificar status da API security: [] responses: '200': description: Serviço no ar content: application/json: schema: $ref: '#/components/schemas/HealthResponse' description: Diz se a API está no ar. Não exige chave. components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: ara_live_... description: 'Chave de API da Arara (`ara_live_...`), enviada no header `Authorization: Bearer ara_live_...`. Crie e gerencie as chaves no painel: https://app.ararahq.com/dashboard/apikeys. O valor completo aparece uma única vez, na criação.' parameters: IdempotencyKeyOptional: in: header name: Idempotency-Key required: false schema: { type: string, maxLength: 255 } description: 'Chave de idempotência, de 1 a 255 caracteres, sem caracteres de controle (fora disso: `400 INVALID_IDEMPOTENCY_KEY`). Derive de um evento do seu sistema (id do pedido, id da tentativa) e reuse a mesma chave nas retentativas.' example: pedido-12345-confirmacao WabaIdHeader: in: header name: X-Waba-Id required: false schema: { type: string, format: uuid } description: Restringe o resultado a uma conta WhatsApp Business (WABA) da organização. TemplateId: in: path name: id required: true schema: { type: string, format: uuid } description: ID do template (UUID). O nome não é aceito aqui. CampaignId: in: path name: id required: true schema: { type: string, format: uuid } description: ID da campanha. CampaignIdPath: in: path name: campaignId required: true schema: { type: string, format: uuid } description: ID da campanha. PhonePath: in: path name: phone required: true schema: type: string pattern: '^\+[1-9]\d{6,14}$' description: Telefone E.164 com `+` (ex. `+5511999999999`). example: '+5511999999999' AnalyticsPeriod: in: query name: period required: false schema: type: string enum: [7d, 30d, 90d] default: 30d description: Valor fora da lista é tratado como `30d`. Page: in: query name: page required: false schema: { type: integer, minimum: 0, default: 0 } description: Página, começando em `0`. FlowId: in: path name: id required: true schema: { type: string, format: uuid } description: ID do formulário. AutomationId: in: path name: id required: true schema: { type: string, format: uuid } description: ID da automação. responses: BadRequest: description: 'Requisição inválida: `VALIDATION_ERROR` (campo obrigatório ausente ou regra do corpo), `INVALID_JSON` (JSON malformado ou tipo errado), `INVALID_PATH_PARAM` ou `INVALID_IDEMPOTENCY_KEY`.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: VALIDATION_ERROR message: O número do recebedor é obrigatório details: {} InvalidPhone: description: '`INVALID_PHONE`: telefone fora do E.164.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: INVALID_PHONE message: Telefone fora do formato E.164. details: {} Unauthorized: description: | Chave inexistente, revogada, expirada, com prefixo errado (`ara_` sem `ara_live_`) ou vinda de IP fora da allowlist. O corpo é o JSON padrão do Spring, sem `error.code`. content: application/json: schema: $ref: '#/components/schemas/SpringError' example: timestamp: '2026-09-15T14:30:00.000+00:00' status: 401 error: Unauthorized path: /v1/messages PaymentRequired: description: '`INSUFFICIENT_FUNDS` (saldo insuficiente na carteira) ou `PLAN_REQUIRED`.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: INSUFFICIENT_FUNDS message: Saldo insuficiente. details: {} Forbidden: description: | Dois casos, com corpos diferentes: - Autenticação: header `Authorization` ausente, Bearer que não começa com `ara_`, chave sem a permissão da rota ou rota que não aceita chave de API. Corpo no formato padrão do Spring, sem `error.code`. - Regra de negócio, no formato `{ "error": { ... } }`: `PLAN_FEATURE_LOCKED`, `PLAN_LIMIT_REACHED`, `ACCOUNT_NOT_ACTIVATED`, `NO_DEDICATED_NUMBER`, `ORGANIZATION_SUSPENDED`, `NUMBER_BLOCKED`, `FRAUD_SUSPECTED`, `RESOURCE_FORBIDDEN`, `VIEWER_READ_ONLY`. content: application/json: schema: anyOf: - $ref: '#/components/schemas/Error' - $ref: '#/components/schemas/SpringError' examples: regraDeNegocio: summary: Regra de negócio value: error: code: PLAN_FEATURE_LOCKED message: Seu plano não inclui este recurso. details: {} autenticacao: summary: Sem header ou sem permissão value: timestamp: '2026-09-15T14:30:00.000+00:00' status: 403 error: Forbidden path: /v1/messages NotFound: description: 'Recurso não encontrado: `TEMPLATE_NOT_FOUND`, `CAMPAIGN_NOT_FOUND` ou `NOT_FOUND`.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: NOT_FOUND message: Recurso não encontrado. details: {} Conflict: description: '`CONFLICT`, `PROVIDER_NOT_RESOLVED` ou `CONCURRENT_MODIFICATION`.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: CONFLICT message: Conflito com o estado atual do recurso. details: {} UnprocessableEntity: description: | A requisição é válida, mas uma regra de negócio impede: `RECIPIENT_OPTED_OUT`, `INVALID_RECIPIENT`, `TEMPLATE_PAUSED`, `TEMPLATE_NOT_SENDABLE`, `TEMPLATE_VARIABLES_MISMATCH`, `CONVERSATION_WINDOW_CLOSED`, `SENDER_NOT_ALLOWED`, `UNPROCESSABLE_ENTITY` ou um código de diagnóstico `ararahq-*` com `details.diagnostic`. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: UNPROCESSABLE_ENTITY message: Não foi possível processar a requisição. details: {} TooManyRequests: description: | Limite atingido: - `RATE_LIMIT_EXCEEDED`: limite geral da API. Traz `Retry-After` e `X-RateLimit-Remaining`. - `SEND_RATE_LIMITED`: teto de envio, inclusive o limite diário da chave. Traz `Retry-After`. - `BATCH_BUSY`: já existe um lote em processamento. Traz `Retry-After`. - `MARKETING_FREQUENCY_EXCEEDED`: o contato já recebeu marketing demais na janela. **Não** traz `Retry-After`. headers: Retry-After: schema: { type: integer } description: Segundos a aguardar antes de repetir a chamada, com a mesma `Idempotency-Key`. Ausente em `MARKETING_FREQUENCY_EXCEEDED`. X-RateLimit-Remaining: schema: { type: integer } description: Requisições restantes na janela atual. Presente em `RATE_LIMIT_EXCEEDED`. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: SEND_RATE_LIMITED message: Limite de envio atingido. Tente de novo em instantes. details: {} RateLimited: description: '`RATE_LIMIT_EXCEEDED`: limite geral da API (1.200 leituras ou 3.000 escritas por minuto, por usuário da chave).' headers: Retry-After: schema: { type: integer } description: Segundos a aguardar antes de repetir a chamada. X-RateLimit-Remaining: schema: { type: integer } description: Requisições restantes na janela atual. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: RATE_LIMIT_EXCEEDED message: Muitas requisições. Tente de novo em instantes. details: {} ServiceUnavailable: description: | Indisponibilidade temporária. Espere o `Retry-After` e repita com a mesma `Idempotency-Key`. - `SEND_TEMPORARILY_UNAVAILABLE`: o envio não pôde ser aceito agora (formato `{ "error": { ... } }`). - Autenticação indisponível: a chave não pôde ser conferida. Corpo no formato padrão do Spring, com `Retry-After: 5`. headers: Retry-After: schema: { type: integer } description: Segundos a aguardar antes de repetir a chamada. content: application/json: schema: anyOf: - $ref: '#/components/schemas/Error' - $ref: '#/components/schemas/SpringError' example: error: code: SEND_TEMPORARILY_UNAVAILABLE message: Envio temporariamente indisponível. Tente de novo em instantes. details: {} FlowNotFound: description: '`FLOW_NOT_FOUND`: formulário inexistente ou de outra organização.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: {code: FLOW_NOT_FOUND, message: Formulário não encontrado., details: {}} FlowStateConflict: description: | A ação não cabe no estado atual: `FLOW_NOT_DRAFT`, `FLOW_NOT_PUBLISHED`, ou `FLOW_NUMBER_GONE` (o número em que o formulário foi criado saiu da organização). content: application/json: schema: $ref: '#/components/schemas/Error' example: error: {code: FLOW_NOT_DRAFT, message: Este formulário já foi publicado., details: {}} AutomationNotFound: description: '`AUTOMATION_NOT_FOUND`: automação inexistente ou de outra organização.' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: {code: AUTOMATION_NOT_FOUND, message: Automação não encontrada., details: {}} AutomationInvalid: description: | `AUTOMATION_TRIGGER_INVALID` (gatilho inexistente), `AUTOMATION_TRIGGER_CONFIG_INVALID` (`button.replied` sem `replyId`), `AUTOMATION_STEP_INVALID` (passo com `type` ou `config` inválido) ou `INVALID_JSON`. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: {code: AUTOMATION_TRIGGER_INVALID, message: Gatilho inválido., details: {}} HookUnavailable: description: '`HOOK_UNAVAILABLE_FOR_TRIGGER`: só automação com gatilho `webhook` tem link próprio.' content: application/json: schema: $ref: '#/components/schemas/Error' schemas: # ---------- Erros ---------- Error: type: object required: [error] properties: error: type: object required: [code, message] properties: code: type: string example: INSUFFICIENT_FUNDS description: Código estável do erro. Use este campo no seu tratamento, não a mensagem. message: type: string nullable: true description: Descrição legível, em português. Pode mudar. details: type: object additionalProperties: true description: Dados extras do erro. Vazio na maioria dos casos. example: error: code: INSUFFICIENT_FUNDS message: Saldo insuficiente. details: {} SpringError: type: object description: Corpo padrão do Spring nas falhas de autenticação (`401`, `403` e `503` do filtro de chave). Não tem `error.code`. properties: timestamp: { type: string, format: date-time } status: type: integer example: 401 error: type: string example: Unauthorized path: type: string example: /v1/messages Pagination: type: object required: [page, size, totalElements, totalPages] properties: page: type: integer description: Página atual, começando em `0`. size: type: integer description: Itens por página. totalElements: { type: integer, format: int64 } totalPages: { type: integer } example: page: 0 size: 50 totalElements: 1 totalPages: 1 # ---------- Account ---------- UserResponse: type: object required: [name, email] properties: name: { type: string } email: { type: string } role: type: string nullable: true description: Hoje sempre `null` nesta rota. emailPending: type: boolean description: Hoje sempre `false` nesta rota. example: name: Maria Silva email: maria@exemplo.com.br role: null emailPending: false OrganizationItem: type: object required: [id, name, role, mode] properties: id: { type: string, format: uuid } slug: type: string nullable: true description: Identificador legível. name: { type: string } role: type: string description: Papel do usuário na organização. mode: type: string enum: [live, test] description: '`live` quando a conta do usuário está ativa; `test` enquanto não está.' OrganizationListResponse: type: object required: [data] properties: data: type: array items: $ref: '#/components/schemas/OrganizationItem' # ---------- Messages ---------- example: data: - id: 7f8a1c2d-4e5f-6789-abcd-ef0123456789 slug: minha-empresa name: Minha Empresa role: OWNER mode: live SendMessageRequest: type: object required: [receiver] description: Envie exatamente um entre `templateName`, `body`, `interactive`, `location` e `reaction`. properties: receiver: type: string description: | Telefone do destinatário. Formato recomendado: E.164 com `+` (`+5511999999999`). Também passam `5511999999999` e `whatsapp:+5511999999999`. A Arara fica só com os dígitos e exige de 7 a 15; fora disso, `422 INVALID_RECIPIENT`. example: '+5511999999999' templateName: type: string nullable: true description: Nome do template aprovado. Pode ser enviado a qualquer hora. example: pedido_confirmado templateVariables: type: array nullable: true items: { type: string } description: | Variáveis posicionais do template, na ordem de `{{1}}`, `{{2}}`... (alias aceito: `variables`). Em template com mídia no cabeçalho, a URL da mídia vai na primeira posição. Não aceita quebra de linha nem tab. example: [João, '12345'] body: type: string nullable: true description: Texto da mensagem. Só com a janela de 24h aberta; fora dela, `422 CONVERSATION_WINDOW_CLOSED`. interactive: allOf: - $ref: '#/components/schemas/InteractivePayload' nullable: true description: Botões, lista, formulário ou cobrança. Só com a janela de 24h aberta. location: allOf: - $ref: '#/components/schemas/LocationPayload' nullable: true description: Localização. Só com a janela de 24h aberta. reaction: allOf: - $ref: '#/components/schemas/ReactionPayload' nullable: true description: Reação com emoji a uma mensagem. charge: allOf: - $ref: '#/components/schemas/ChargePayload' nullable: true description: Cobrança que acompanha um template com botão `CHARGE`. Use junto com `templateName`. sender: type: string nullable: true description: Número da organização que envia. Omitido, usa o número padrão. Número que não é seu responde `422 SENDER_NOT_ALLOWED`. replyTo: type: string nullable: true description: ID (`ara_msg_...`) da mensagem citada na resposta. scheduled_at: type: string format: date-time nullable: true description: | Agenda o envio (ISO 8601). Data no futuro deixa a mensagem em `SCHEDULED` até o horário. Data no passado envia na hora. Aceita também o nome `scheduledAt`. example: '2027-01-15T14:00:00-03:00' smartLinkParam: type: string nullable: true description: Parâmetro do botão de smart link do template. smartLinkUrl: type: string nullable: true description: URL de destino do botão de smart link do template. type: type: string nullable: true enum: [template, text, interactive, location, reaction] description: Opcional. Quando omitido, é inferido do conteúdo enviado. mode: type: string nullable: true enum: [LIVE] description: Opcional. Todo envio por chave de API é real (`LIVE`). Não existe modo de teste. media_url: type: string nullable: true deprecated: true description: Legado (aceita também `mediaUrl`). Use a primeira posição de `templateVariables`. MessageResponse: type: object required: [status, mode, sender, receiver] properties: id: type: string nullable: true description: ID público da mensagem. Use para consultar e para casar com o webhook. example: ara_msg_k9Bf2Xq status: type: string enum: [PENDING, SCHEDULED, PROCESSING, SENT, DELIVERED, READ, FAILED, CANCELED] description: | Estado atual, sempre em maiúsculas: - `PENDING`: criada, na fila. - `SCHEDULED`: agendada. - `PROCESSING`: em processamento. - `SENT`: aceita pelo provedor. - `DELIVERED`: entregue no aparelho. - `READ`: lida. - `FAILED`: falhou. Veja `reason`. - `CANCELED`: cancelada antes do envio. O webhook `message.status_updated` usa os mesmos estados em minúsculas, com `PENDING` virando `queued`. mode: type: string enum: [LIVE] description: Todo envio por chave de API é `LIVE`. sender: type: string description: Número que enviou. receiver: type: string description: Número do destinatário. body: type: string nullable: true description: Conteúdo final enviado, com as variáveis aplicadas. Pode vir `null`. cost: type: number nullable: true description: Custo em reais. Pode vir `null`. Os valores dos exemplos são ilustrativos. reason: type: string nullable: true description: Motivo da falha quando `status` é `FAILED`. example: id: ara_msg_k9Bf2Xq status: PENDING mode: LIVE sender: whatsapp:+5511888888888 receiver: whatsapp:+5511999999999 body: Oi João, seu pedido 12345 foi confirmado! cost: 0.068 reason: null InteractivePayload: type: object required: [type, body] properties: type: type: string enum: [button, list, flow, charge, charge_status] description: | - `button`: botões de resposta (`buttons`). - `list`: lista de opções (`buttonText` e `sections`). - `flow`: abre um formulário (`flowId`). - `charge`: cartão de cobrança (`charge`). - `charge_status`: baixa de uma cobrança já enviada (`chargeStatus`). body: type: string description: Texto da mensagem. buttonText: type: string nullable: true description: Em `list`, texto do botão que abre a lista (obrigatório). Em `flow`, texto do botão que abre o formulário (padrão `Responder`, até 20 caracteres). sections: type: array nullable: true description: Só em `list`. items: $ref: '#/components/schemas/InteractiveSection' buttons: type: array nullable: true description: Só em `button`. A resposta do cliente chega pelo webhook `message.received`. items: $ref: '#/components/schemas/InteractiveButton' flowId: type: string format: uuid nullable: true description: Só em `flow`. O `id` do formulário, de `GET /v1/flows`. charge: allOf: - $ref: '#/components/schemas/ChargePayload' nullable: true description: Só em `charge`. chargeStatus: allOf: - $ref: '#/components/schemas/ChargeStatusPayload' nullable: true description: Só em `charge_status`. 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 description: Identificador da opção. Volta no webhook quando o cliente escolhe. title: { type: string } description: type: string nullable: true InteractiveButton: type: object required: [id, title] properties: id: type: string description: Identificador do botão. Volta no webhook quando o cliente toca. title: type: string description: Texto do botão. LocationPayload: type: object required: [latitude, longitude] properties: latitude: { type: number, format: double } longitude: { type: number, format: double } name: type: string nullable: true description: Nome do local. address: type: string nullable: true description: Endereço exibido. ReactionPayload: type: object required: [emoji, messageId] properties: emoji: type: string description: Emoji da reação. messageId: type: string description: ID (`ara_msg_...`) da mensagem que recebe a reação. ChargePayload: type: object required: [referenceId, items] description: | Cobrança enviada como cartão de pagamento. Valores sempre em centavos. Informe um meio de pagamento por cobrança: `pix`, `paymentLink` ou `boleto`. O pagamento é gerado no seu banco ou PSP; a Arara só entrega o cartão. Guia em [Cobranças](/cobrancas). properties: referenceId: type: string description: Seu identificador da cobrança. É com ele que você dá baixa depois. items: type: array items: $ref: '#/components/schemas/ChargeItem' pix: type: object nullable: true required: [code, key, keyType, merchantName] properties: code: type: string description: Pix copia e cola (BR Code) gerado no seu banco ou PSP. key: { type: string } keyType: type: string enum: [CPF, CNPJ, EMAIL, PHONE, EVP] merchantName: { type: string } paymentLink: type: object nullable: true required: [url] properties: url: type: string description: Link de pagamento, em `https`. boleto: type: object nullable: true required: [digitableLine] properties: digitableLine: type: string description: Linha digitável do boleto, só dígitos. shippingCents: type: integer format: int64 default: 0 discountCents: type: integer format: int64 default: 0 expiresAt: type: integer format: int64 nullable: true description: Vencimento, em epoch segundos. Pelo menos 5 minutos à frente. expiresDescription: type: string nullable: true description: Texto exibido junto do vencimento. Só vale com `expiresAt`. imageUrl: type: string nullable: true description: Foto no topo do cartão. Sem ela, vale a do primeiro item com foto. ChargeItem: type: object required: [name, amountCents] properties: name: { type: string } amountCents: type: integer format: int64 description: Valor unitário, em centavos. quantity: type: integer default: 1 id: type: string nullable: true imageUrl: type: string nullable: true description: Foto do item. Com foto, o `id` do item é ignorado. ChargeStatusPayload: type: object required: [referenceId, status] properties: referenceId: type: string description: O `referenceId` da cobrança enviada. status: type: string enum: [PAID, FAILED, CANCELED, EXPIRED] description: type: string nullable: true BatchMessageRequest: type: object required: [templateName, messages] properties: templateName: type: string description: Nome do template aprovado, igual para todo o lote. messages: type: array minItems: 1 maxItems: 1000 items: $ref: '#/components/schemas/BatchMessageItem' description: De 1 a 1.000 destinatários. BatchMessageItem: type: object required: [receiver] properties: receiver: type: string description: Telefone do destinatário, E.164 com `+`. example: '+5511999999999' templateVariables: type: array nullable: true items: { type: string } description: Variáveis posicionais deste destinatário. Aceita o alias `variables`. smartLinkParam: type: string nullable: true description: Parâmetro do botão de smart link do template. smartLinkUrl: type: string nullable: true description: URL de destino do botão de smart link do template. mediaUrl: type: string nullable: true description: URL da mídia do cabeçalho para este destinatário. BatchMessageResponse: type: object required: [batchId, templateName, total, accepted, totalCost, messages] properties: batchId: type: string description: Use em `GET /v1/messages?batchId=` para acompanhar o lote. templateName: { type: string } total: type: integer description: Destinatários recebidos. accepted: type: integer description: Mensagens aceitas para envio. totalCost: type: number description: Custo total em reais. messages: type: array items: $ref: '#/components/schemas/BatchMessageItemResponse' example: batchId: ara_msg_b7C1d2E templateName: pedido_confirmado total: 2 accepted: 2 totalCost: 0.136 messages: - id: ara_msg_a1B2c3D receiver: whatsapp:+5511999999999 status: PENDING cost: 0.068 - id: ara_msg_e4F5g6H receiver: whatsapp:+5511888888888 status: PENDING cost: 0.068 BatchMessageItemResponse: type: object required: [receiver, status] properties: id: type: string nullable: true receiver: { type: string } status: { type: string } cost: type: number nullable: true # ---------- Templates ---------- CreateTemplateRequest: type: object required: [name, category, body] properties: name: type: string description: Nome do template. Único por conta WhatsApp Business. example: pedido_confirmado category: type: string enum: [MARKETING, UTILITY, AUTHENTICATION] description: Categoria avaliada pela Meta. Em `AUTHENTICATION` o conteúdo é fixo (veja a descrição do endpoint). body: type: string description: Texto com variáveis `{{1}}`, `{{2}}`... Não pode começar nem terminar com variável. language: type: string default: pt_BR description: Idioma do template. header: type: string nullable: true description: 'Cabeçalho: texto simples ou URL de mídia.' headerType: type: string nullable: true enum: [text, media, document] description: Tipo do cabeçalho. footer: type: string nullable: true description: Rodapé. buttons: type: array nullable: true items: $ref: '#/components/schemas/TemplateButton' description: Até 10 botões, com no máximo 2 de link (`URL` + `SMART_LINK`), 1 de telefone e 1 de formulário. samples: type: object nullable: true additionalProperties: { type: string } description: 'Exemplo por número da variável (`{"1": "João"}`). A Meta usa na revisão. Precisa ser objeto: array responde `400 INVALID_JSON`.' variableExamples: type: array nullable: true items: { type: string } description: Legado. Prefira `samples`. carouselCards: type: array nullable: true items: $ref: '#/components/schemas/CarouselCard' description: Carrossel, de 2 a 10 cards. Todos os cards precisam ter a mesma estrutura de botões. minItems: 2 maxItems: 10 TemplateButton: type: object required: [type, text] properties: type: type: string enum: [QUICK_REPLY, URL, PHONE_NUMBER, SMART_LINK, COPY_CODE, FLOW, CHARGE] description: | - `QUICK_REPLY`: resposta rápida. - `URL`: abre um link. Aceita variável no fim da URL. - `PHONE_NUMBER`: liga para o número em `phone`. - `SMART_LINK`: link rastreável da Arara. - `COPY_CODE`: copia um código. - `FLOW`: abre o formulário de `flowId`. Um por template. - `CHARGE`: botão de cobrança. A cobrança vai no campo `charge` do envio. text: type: string maxLength: 20 description: Texto do botão, até 20 caracteres. url: type: string nullable: true description: Só em `URL` e `SMART_LINK`. phone: type: string nullable: true description: Só em `PHONE_NUMBER`. flowId: type: string format: uuid nullable: true description: Só em `FLOW`. O `id` do formulário, criado no mesmo número do template. extraConfig: type: object nullable: true additionalProperties: true CarouselCard: type: object required: [mediaUrl, body] properties: mediaUrl: type: string description: URL da imagem ou do vídeo do card. body: type: string description: Texto do card. buttons: type: array nullable: true items: $ref: '#/components/schemas/TemplateButton' description: Até 2 botões por card. TemplateResponse: type: object required: [id, name, formattedName, category, language, providerName, providerTemplateId, providerStatus, availableForSending, structureJson, createdAt] properties: id: type: string format: uuid description: ID do template (UUID). name: { type: string } formattedName: { type: string } category: type: string description: Categoria atual. A Meta pode mudar a categoria na revisão. originalCategory: type: string nullable: true description: Categoria pedida na criação. language: { type: string } providerName: type: string example: GUPSHUP providerTemplateId: { type: string } providerStatus: type: string example: APPROVED description: | Status de aprovação: - `PENDING`: em revisão. - `APPROVED`: pronto para enviar. - `REJECTED`: reprovado. Veja `rejectionReason`. - `PAUSED`: pausado pela Meta ou pela proteção automática da Arara. O envio responde `422 TEMPLATE_PAUSED`. - `DISABLED`: desativado. O envio responde `422 TEMPLATE_NOT_SENDABLE`. rejectionReason: type: string nullable: true availableForSending: type: boolean description: '`false` quando o template foi aprovado em um número que a organização não usa mais. Recrie o template no número ativo.' unavailableReason: type: string nullable: true bodyPreview: type: string nullable: true description: Corpo do template, com os marcadores de variável. structureJson: type: object additionalProperties: true usageGuide: type: object nullable: true additionalProperties: true variablesSchema: type: object nullable: true additionalProperties: { type: string } description: Exemplos por número da variável. createdAt: { type: string, format: date-time } updatedAt: type: string format: date-time nullable: true example: id: a7f3c1e2-4b5d-6789-abcd-ef0123456789 name: pedido_confirmado formattedName: pedido_confirmado category: UTILITY originalCategory: UTILITY language: pt_BR providerName: GUPSHUP providerTemplateId: 4c1f0e9a-2b3d-4e5f-8a6b-7c8d9e0f1a2b providerStatus: PENDING rejectionReason: null availableForSending: true unavailableReason: null bodyPreview: 'Oi {{1}}, seu pedido {{2}} foi confirmado. Total: R$ {{3}}.' structureJson: {} usageGuide: message: Use o objeto 'variables' no envio da mensagem para substituir os placeholders. endpoint: POST /v1/messages variablesSchema: '1': João '2': '12345' '3': 147,90 createdAt: '2026-09-15T14:30:00Z' updatedAt: null TemplatePage: type: object required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/TemplateResponse' pagination: $ref: '#/components/schemas/Pagination' TemplateStatusResponse: type: object required: [status] properties: status: type: string description: '`PENDING`, `APPROVED`, `REJECTED`, `PAUSED` ou `DISABLED`.' rejectionReason: type: string nullable: true description: Motivo informado pela Meta quando `REJECTED`. category: type: string nullable: true example: status: REJECTED rejectionReason: INVALID_FORMAT category: UTILITY TemplateAnalyticsResponse: type: object required: [templateId, templateName, period, sent, delivered, read, failed, deliveryRate, readRate] properties: templateId: { type: string } templateName: { type: string } period: { type: string } sent: type: integer format: int64 description: Soma de enviadas, entregues e lidas. delivered: { type: integer, format: int64 } read: { type: integer, format: int64 } failed: { type: integer, format: int64 } deliveryRate: type: string description: Percentual com 1 casa decimal, como string (ex. `"97.6"`), calculado sobre `sent + failed`. readRate: type: string description: Percentual com 1 casa decimal, como string. example: templateId: a7f3c1e2-4b5d-6789-abcd-ef0123456789 templateName: pedido_confirmado period: 30d sent: 1250 delivered: 1230 read: 890 failed: 10 deliveryRate: '97.6' readRate: '70.6' TemplateAnalyticsBatchItem: type: object properties: templateName: { type: string } period: { type: string } sent: { type: integer, format: int64 } delivered: { type: integer, format: int64 } read: { type: integer, format: int64 } failed: { type: integer, format: int64 } deliveryRate: { type: string } readRate: { type: string } LibraryTemplateResponse: type: object required: [elementName, body, languageCode] properties: elementName: { type: string } body: { type: string } sampleText: type: string nullable: true header: type: string nullable: true topic: type: string nullable: true usecase: type: string nullable: true languageCode: { type: string } buttons: description: Botões no formato do provedor (pode vir `null`). CreateLibraryTemplateRequest: type: object required: [name, libraryTemplateName] properties: name: type: string description: Nome do seu template. libraryTemplateName: type: string description: '`elementName` do item da biblioteca.' languageCode: type: string default: pt_BR # ---------- Campaigns ---------- description: Idioma do item da biblioteca. CampaignStatus: type: string enum: [SCHEDULED, INGESTING, AB_TESTING, COMPLETED, COMPLETED_WITH_ERRORS, FAILED, CANCELED] description: | - `SCHEDULED`: agendada, aguardando o horário. - `INGESTING`: processando contatos e enfileirando envios. - `AB_TESTING`: teste A/B em andamento. - `COMPLETED`: todos os contatos processados. - `COMPLETED_WITH_ERRORS`: processada, com contatos bloqueados. - `FAILED`: falha geral no processamento. - `CANCELED`: cancelada. CampaignRequest: type: object required: [name, templateName, contacts] properties: name: type: string description: Nome interno da campanha. templateName: type: string description: Nome ou ID do template aprovado. Com `abTest`, é a variante A. sender: type: string nullable: true description: Número que envia. Omitido, usa o padrão da organização. contacts: type: array minItems: 1 items: $ref: '#/components/schemas/CampaignContactRequest' description: Destinatários, no mínimo 1. abTest: $ref: '#/components/schemas/CampaignAbConfig' scheduledAt: type: string format: date-time nullable: true description: Agenda o envio (ISO 8601), entre 5 minutos e 30 dias no futuro. CampaignContactRequest: type: object required: [to] properties: to: type: string description: Telefone E.164 com `+`. example: '+5511999999999' variables: type: array default: [] items: { type: string } description: Variáveis posicionais do template, em **array** (`["João", "30"]`), não em objeto. CampaignAbConfig: type: object required: [variantBTemplateName] properties: variantBTemplateName: type: string description: Template da variante B. metric: type: string enum: [DELIVERED, READ, CLICKED, CONVERTED] default: CLICKED description: Métrica que decide a vencedora. samplePct: type: integer default: 20 description: Porcentagem dos contatos que entra no teste. O restante espera a vencedora. splitPct: type: integer minimum: 1 maximum: 99 default: 50 description: Divisão entre A e B dentro da amostra, de 1 a 99. decisionWindowMinutes: type: integer default: 240 description: Minutos até decidir a vencedora. autopilot: type: boolean default: true description: Envia o restante pela vencedora automaticamente. CampaignResponse: type: object required: [id, name, status, totalMessages, totalCost] properties: id: { type: string, format: uuid } name: { type: string } status: $ref: '#/components/schemas/CampaignStatus' totalMessages: { type: integer } totalCost: type: number description: Valor já debitado da carteira, em reais. Os valores dos exemplos são ilustrativos. scheduledAt: type: string format: date-time nullable: true example: id: 7b3789fb-4c5d-6789-abcd-ef0123456789 name: Black Friday 2026 status: INGESTING totalMessages: 2 totalCost: 0.767 scheduledAt: null CampaignListItem: type: object required: [id, name, status, templateName, totalMessages, sentCount, deliveredCount, readCount, failedCount, totalCost] properties: id: { type: string, format: uuid } name: { type: string } status: { type: string } templateName: { type: string } totalMessages: { type: integer } sentCount: { type: integer } deliveredCount: { type: integer } readCount: { type: integer } failedCount: { type: integer } totalCost: { type: number } scheduledAt: type: string format: date-time nullable: true 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 } example: content: - id: 7b3789fb-4c5d-6789-abcd-ef0123456789 name: Black Friday 2026 status: COMPLETED templateName: bf_oferta_geral totalMessages: 50000 sentCount: 48500 deliveredCount: 47200 readCount: 38900 failedCount: 900 totalCost: 19175.0 scheduledAt: null createdAt: '2026-11-27T18:00:00Z' totalPages: 1 totalElements: 1 CampaignDetailResponse: type: object required: [id, name, status, templateName, totalMessages, sentCount, deliveredCount, readCount, failedCount, 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 } failedCount: { type: integer } clickedCount: type: integer description: Cliques em smart links da campanha. convertedCount: type: integer description: Conversões atribuídas por smart links. convertedValue: { type: number } replyCount: type: integer description: Conversas originadas pela campanha. holdoutCount: type: integer description: Contatos aguardando a vencedora do A/B. blockedCount: type: integer description: Contatos bloqueados no envio (opt-out, número inválido, limite de frequência). blockReasons: type: array items: type: object properties: motivo: { type: string } quantidade: { type: integer, format: int64 } refundCount: { type: integer } refundValue: type: number description: Total estornado para a carteira, em reais. totalCost: { type: number } 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 example: id: 7b3789fb-4c5d-6789-abcd-ef0123456789 name: Black Friday 2026 status: COMPLETED_WITH_ERRORS templateName: bf_oferta_geral templateBody: Oi {{1}}, oferta de {{2}}% só hoje! totalMessages: 50000 sentCount: 48500 deliveredCount: 47200 readCount: 38900 failedCount: 900 clickedCount: 4120 convertedCount: 312 convertedValue: 87650.0 replyCount: 1840 holdoutCount: 0 blockedCount: 600 blockReasons: - motivo: Contato pediu para não receber quantidade: 600 refundCount: 1500 refundValue: 575.25 totalCost: 19175.0 scheduledAt: null startedAt: '2026-11-27T18:00:12Z' finishedAt: '2026-11-27T18:23:14Z' createdAt: '2026-11-27T18:00:00Z' CampaignEstimateRequest: type: object required: [templateName, phones] properties: templateName: type: string description: Nome ou ID do template. phones: type: array minItems: 1 items: { type: string } description: Telefones E.164 com `+`. O preço é calculado por país de destino. CampaignEstimateResponse: type: object required: [templateCategory, recipientCount, templateCost, araraFee, unitPrice, totalCost] properties: templateCategory: { type: string } recipientCount: { type: integer } templateCost: type: number description: Custo Meta total, em reais. araraFee: type: number description: Taxa Arara total, em reais. unitPrice: type: number description: Preço médio por mensagem. totalCost: type: number description: Total a debitar, em reais. markets: type: array description: Quebra por mercado. Vazio no `GET`. items: $ref: '#/components/schemas/CampaignMarketCost' example: templateCategory: MARKETING recipientCount: 2 templateCost: 0.59 araraFee: 0.177 unitPrice: 0.3835 totalCost: 0.767 markets: - market: BR count: 2 unitPrice: 0.3835 metaCostUnit: 0.295 subtotal: 0.767 CampaignMarketCost: type: object properties: market: { type: string } count: { type: integer } unitPrice: { type: number } metaCostUnit: { type: number } subtotal: { type: number } CreateAbTestRequest: type: object required: [variantATemplateName, variantBTemplateName, metric] properties: variantATemplateName: { type: string } variantBTemplateName: { type: string } metric: type: string enum: [DELIVERED, READ, CLICKED, CONVERTED] samplePct: type: integer default: 20 splitPct: type: integer minimum: 1 maximum: 99 default: 50 decisionWindowMinutes: type: integer default: 240 autopilot: type: boolean default: false description: Nesta rota o padrão é `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 nullable: true relativeLiftPct: type: number nullable: true variantAMetricsJson: type: string nullable: true description: JSON serializado em string. 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 description: Nome do contato. Obrigatório, até 255 caracteres. phone: type: string pattern: '^\+[1-9]\d{6,14}$' example: '+5511999999999' description: Telefone E.164 com `+`. É a chave do contato. email: type: string maxLength: 255 nullable: true description: No lote, substitui o email gravado. Omitir apaga o email de um contato existente. attributes: type: object nullable: true additionalProperties: true description: Objeto livre com dados do seu sistema. No lote, substitui o que está gravado. ContactPatchRequest: type: object properties: name: type: string maxLength: 255 nullable: true description: Até 255 caracteres. Vazio é ignorado. email: type: string maxLength: 255 nullable: true description: Até 255 caracteres. String vazia remove o email. tags: type: array nullable: true maxItems: 20 items: { type: string } description: Substitui a lista inteira. Até 20 tags. ContactResponse: type: object required: [id, name, phone, tags, createdAt, lifecycle, source, outboundCount, inboundCount] properties: id: { type: string, format: uuid } name: { type: string } phone: { type: string } email: type: string nullable: true attributes: type: object nullable: true additionalProperties: true tags: type: array items: { type: string } createdAt: { type: string } lifecycle: type: string description: 'Estágio do contato: `NEW`, `ENGAGED`, `SILENT`, `DORMANT` ou `OPTED_OUT`.' source: type: string description: De onde o contato veio (ex. `API`). outboundCount: { type: integer, format: int64 } inboundCount: { type: integer, format: int64 } firstSeenAt: type: string nullable: true lastOutboundAt: type: string nullable: true lastInboundAt: type: string nullable: true lastMessageAt: type: string nullable: true optOutAt: type: string nullable: true description: Quando o contato pediu para não receber. lastTemplateName: type: string nullable: true description: Último template enviado ao contato. example: id: 1f2e3d4c-5b6a-7890-abcd-ef1234567890 name: João Silva phone: '+5511999999999' email: joao@exemplo.com attributes: plano: pro tags: - vip createdAt: '2026-09-01T14:30:00Z' lifecycle: ENGAGED source: API outboundCount: 12 inboundCount: 3 firstSeenAt: '2026-09-01T14:30:00Z' lastOutboundAt: '2026-09-14T10:00:00Z' lastInboundAt: '2026-09-14T10:05:00Z' lastMessageAt: '2026-09-14T10:05:00Z' optOutAt: null lastTemplateName: pedido_confirmado 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, errors] properties: importId: type: string format: uuid description: Identifica a chamada nos logs. Não há rota para consultá-lo. created: { type: integer } updated: { type: integer } skipped: { type: integer } errors: type: array items: type: object required: [index, reason] properties: index: { type: integer } phone: type: string nullable: true reason: { type: string } example: importId: fa1b2c3d-4567-89ab-cdef-0123456789ab created: 1 updated: 0 skipped: 1 errors: - index: 1 phone: '5511888888888' reason: Telefone 5511888888888 fora do formato E.164. ContactsStatsResponse: type: object required: [total, newCount, engaged, silent, dormant, optedOut] properties: total: { type: integer, format: int64 } newCount: { type: integer, format: int64 } engaged: { type: integer, format: int64 } silent: { type: integer, format: int64 } dormant: { type: integer, format: int64 } optedOut: { type: integer, format: int64 } example: total: 12450 newCount: 3100 engaged: 5200 silent: 2800 dormant: 1263 optedOut: 87 ContactMessagesResponse: type: object required: [phone, total, messages] properties: phone: { type: string } total: { type: integer } messages: type: array items: type: object properties: id: { type: string, format: uuid } direction: { type: string } status: { type: string } templateName: type: string nullable: true body: type: string nullable: true createdAt: { type: string } LgpdExport: type: object required: [phoneHash, phone, exportedAt, conversations, messages] properties: phoneHash: type: string description: SHA-256 do telefone. phone: { type: string } exportedAt: { type: string, format: date-time } contact: type: object nullable: true additionalProperties: true conversations: type: array items: type: object additionalProperties: true messages: type: array items: type: object additionalProperties: true LgpdDeletionResult: type: object required: [phoneHash, deletedAt, contactDeleted, conversationsRedacted, messagesRedacted, optOutRegistered] properties: phoneHash: { type: string } deletedAt: { type: string, format: date-time } contactDeleted: { type: boolean } conversationsRedacted: { type: integer } messagesRedacted: { type: integer } optOutRegistered: { type: boolean } example: phoneHash: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 deletedAt: '2026-09-15T14:30:00Z' contactDeleted: true conversationsRedacted: 1 messagesRedacted: 42 optOutRegistered: true # ---------- Opt-outs ---------- OptOutItem: type: object required: [phone, channel] properties: phone: { type: string } channel: type: string example: WHATSAPP reason: type: string nullable: true createdAt: type: string nullable: true # ---------- Conversations ---------- example: phone: '+5511999999999' channel: WHATSAPP reason: pediu por email createdAt: '2026-09-15T14:30:00Z' 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 nullable: true qualifierCompletedAt: type: string format: date-time nullable: true lastMessagePreview: type: string nullable: true lastMessageDirection: type: string 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 nullable: true direction: type: string enum: [INBOUND, OUTBOUND] body: type: string nullable: true status: { type: string } reason: type: string nullable: true sender: { type: string } receiver: { type: string } messageType: type: string nullable: true createdAt: type: string format: date-time nullable: true mediaUrl: type: string nullable: true mediaContentType: type: string nullable: true mediaSummary: 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, message] properties: id: type: string nullable: true status: { type: string } reason: type: string nullable: true message: { type: string } WindowStatusRequest: type: object required: [phones] properties: phones: type: array 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 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 } # ---------- Smart links ---------- CreateSmartLinkRequest: type: object required: [name, phoneNumber] properties: name: { type: string } phoneNumber: { type: string } defaultText: type: string nullable: true qrCodeColor: type: string default: BLACK UpdateSmartLinkRequest: type: object properties: name: type: string nullable: true defaultText: type: string nullable: true qrCodeColor: type: string nullable: true SmartLinkResponse: type: object required: [id, name, phoneNumber, qrCodeColor, code, shortUrl, clicks] properties: id: { type: string, format: uuid } name: { type: string } phoneNumber: { type: string } defaultText: type: string nullable: true qrCodeColor: { type: string } code: { type: string } shortUrl: type: string description: URL pública (`.../l/{code}`). createdAt: type: string format: date-time nullable: true clicks: { type: integer, format: int64 } SmartLinkPage: type: object required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/SmartLinkResponse' pagination: $ref: '#/components/schemas/Pagination' # ---------- 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 enum: [ACTIVE, PENDING_REVIEW, SUSPENDED, DISABLED] qualityScore: type: string enum: [HIGH, MEDIUM, LOW, FLAGGED, UNKNOWN] description: Qualidade do número segundo a Meta. messagingTier: type: string enum: [TIER_50, TIER_250, TIER_1K, TIER_2K, TIER_10K, TIER_100K, UNLIMITED, UNKNOWN] description: | Tier informado pela Meta (conversas iniciadas pelo número por dia): `TIER_50` (50), `TIER_250` (250), `TIER_1K` (1.000), `TIER_2K` (2.000), `TIER_10K` (10.000), `TIER_100K` (100.000) e `UNLIMITED` (sem limite); `UNKNOWN` é tier ainda não sincronizado. O teto diário que a Arara aplica no envio é o do tier, exceto `TIER_2K` e `UNKNOWN`, tratados como 1.000. verifiedAt: type: string format: date-time nullable: true lastHealthCheckAt: type: string format: date-time nullable: true provider: { type: string } createdAt: type: string format: date-time nullable: true messagesLast7d: { type: integer, format: int64 } messagesLast30d: { type: integer, format: int64 } deliveredLast30d: { type: integer, format: int64 } provisionedByArara: { type: boolean } wabaId: type: string nullable: true isCoexistence: type: boolean nullable: true NumbersResponse: type: object required: [numbers, slot] properties: numbers: type: array items: $ref: '#/components/schemas/NumberCard' slot: type: object required: [used, max, planLabel, atCap, noEntitlement, monthlyPriceCents, monthlyTotalCents] properties: used: { type: integer } max: { type: integer } planLabel: { type: string } atCap: { type: boolean } noEntitlement: { type: boolean } monthlyPriceCents: { type: integer } monthlyTotalCents: { type: integer } # ---------- Business profile ---------- BusinessProfilePatch: type: object properties: displayName: type: string maxLength: 120 vertical: type: string 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 } aboutShort: type: string maxLength: 140 email: type: string maxLength: 160 websites: type: array items: { type: string } businessHours: type: object additionalProperties: true profilePhotoUrl: { type: string } awayMessage: { type: string } 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 } metaSyncedAt: type: string format: date-time nullable: true metaSyncError: type: string nullable: true updatedAt: type: string format: date-time nullable: true # ---------- Wallet ---------- WalletBalanceResponse: type: object required: [balance, currency, mode] properties: balance: type: number description: Saldo em reais. currency: type: string example: BRL mode: type: string example: LIVE WalletTransaction: type: object required: [id, amount, type, mode, hasReceipt] properties: id: { type: string } amount: type: number description: Valor em reais. type: type: string enum: [CHARGE, REFUND, TOPUP, BONUS, DEPOSIT, USAGE, SUBSCRIPTION_FEE, ADMIN_DEBIT] description: type: string nullable: true referenceId: type: string nullable: true mode: type: string example: LIVE createdAt: type: string format: date-time nullable: true hasReceipt: { type: boolean } 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 } AutoRechargeSettings: type: object required: [enabled, threshold, amount] properties: enabled: { type: boolean } threshold: type: number description: Saldo (R$) que aciona a recarga. amount: type: number 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 nullable: true minimum: 5 maximum: 1000 amount: type: number nullable: true minimum: 20 maximum: 5000 RechargeEstimate: type: object required: [amountCents, planLabel, limits, firstRechargeBonus, estimates] properties: amountCents: { type: integer } planLabel: { type: string } limits: type: object properties: minCents: { type: integer } maxCents: { type: integer } firstRechargeBonus: type: object properties: applicable: { type: boolean } percent: { type: integer } bonusCents: { type: integer } totalCreditCents: { type: integer } estimates: type: object description: Chaves `marketing`, `utility` e `authentication`. additionalProperties: type: object properties: count: { type: integer } unitPriceCents: { type: integer } # ---------- Pricing ---------- PricingResponse: type: object required: [currency, model, categories, markets] properties: currency: type: string example: BRL model: type: string example: PER_MESSAGE_BY_MARKET categories: type: array items: type: object required: [category, priceBrl, source] properties: category: type: string enum: [MARKETING, UTILITY, AUTHENTICATION] priceBrl: { type: number } source: type: string example: PLAN_REFERENCE markets: type: array items: type: object required: [market, category, costUsd] properties: market: { type: string } category: { type: string } costUsd: { type: number } PlanInfoResponse: type: object required: [current, monthlyPriceCents, marketingPriceCents, utilityPriceCents, authPriceCents, features] properties: current: type: string enum: [EMBARQUE, ESSENTIAL, VOO, VOO_ASSISTIDO, FREE, POC, DECOLAGEM, VOO_SOLO, PRO, ENTERPRISE] description: | Planos vendidos hoje: `EMBARQUE` (Embarque), `ESSENTIAL` (Essencial), `VOO` (Voo) e `VOO_ASSISTIDO` (Primeira Classe). Os demais são legados e podem aparecer em contas antigas: `FREE`, `POC` (Piloto), `DECOLAGEM`, `VOO_SOLO` (Solo), `PRO` e `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 # ---------- Phone lookup ---------- LookupResult: type: object required: [phoneNumber] properties: phoneNumber: { type: string } isMobile: type: boolean nullable: true lineType: type: string nullable: true carrier: type: string nullable: true hasWhatsapp: type: boolean nullable: true costCharged: type: number nullable: true error: type: string nullable: true description: '`INSUFFICIENT_FUNDS` ou `FAILED` no lote.' # ---------- Recovery ---------- RecoveryEndpointResponse: type: object required: [code, hmacSecret, active, url] properties: code: { type: string } hmacSecret: type: string description: Segredo HMAC da ingestão. Trate como senha. active: { type: boolean } url: type: string description: URL pública de ingestão (`.../l/{code}`). 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 templateStatus: type: string nullable: true rejectionReason: type: string 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 # ---------- Dashboard ---------- DashboardMessageItem: type: object properties: id: { type: string } status: type: string enum: [PENDING, SCHEDULED, PROCESSING, SENT_TO_PROVIDER, SENT, DELIVERED, READ, FAILED, CANCELED] mode: { type: string } 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 } DashboardMessageDetail: allOf: - $ref: '#/components/schemas/DashboardMessageItem' - type: object properties: templateVariablesJson: { type: string } body: { type: string } processedContent: { type: string } mediaUrl: { type: string } direction: { type: string } processingStart: { type: string } processingEnd: { type: string } 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 } MetricsWindow: type: object 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 formatado em string. totalCost: { type: number } DashboardMetricsResponse: allOf: - $ref: '#/components/schemas/MetricsWindow' - type: object properties: since: type: string description: Data da primeira mensagem (vazio sem histórico). window7d: $ref: '#/components/schemas/MetricsWindow' previous7d: $ref: '#/components/schemas/MetricsWindow' mode: type: string example: LIVE # ---------- Legacy ---------- 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 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 # ---------- 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 FlowFieldType: type: string enum: [TEXT, EMAIL, PHONE, NUMBER, LONG_TEXT, SELECT, MULTI_SELECT, DATE, OPT_IN] description: Texto, e-mail, telefone, número, texto longo, escolha única, múltipla escolha, data ou aceite. FlowCategory: type: string enum: [SIGN_UP, LEAD_GENERATION, SURVEY, APPOINTMENT_BOOKING, CONTACT_US, OTHER] FlowStatus: type: string enum: [DRAFT, PUBLISHED, DEPRECATED] description: | `DRAFT`: criado, mas a publicação falhou (veja `publishError`). `PUBLISHED`: no ar, pode ser enviado. `DEPRECATED`: desativado. CreateFlowRequest: type: object required: [name, category, title, fields] properties: name: type: string maxLength: 120 description: Nome interno, único por número. Só você vê. category: $ref: '#/components/schemas/FlowCategory' title: type: string maxLength: 30 description: Aparece no topo do formulário. intro: type: string maxLength: 300 nullable: true description: Texto de introdução, abaixo do título. submitLabel: type: string maxLength: 30 default: Enviar description: Texto do botão de envio. fields: type: array minItems: 1 maxItems: 10 description: De 1 a 10 perguntas, todas na mesma tela. items: $ref: '#/components/schemas/FlowFieldRequest' FlowFieldRequest: type: object required: [label, type] properties: label: type: string maxLength: 80 description: Texto da pergunta. type: $ref: '#/components/schemas/FlowFieldType' required: type: boolean default: true description: Pergunta opcional sem resposta chega como string vazia no webhook. options: type: array nullable: true minItems: 2 maxItems: 20 items: { type: string, maxLength: 30 } description: Só em `SELECT` e `MULTI_SELECT`. De 2 a 20 opções, até 30 caracteres cada. key: type: string nullable: true maxLength: 40 pattern: '^[a-z][a-z0-9_]*$' description: | Nome do campo no webhook (`fields.email`). Minúsculas, números e `_`, começando por letra. Sem ela, nasce do texto da pergunta. Não muda depois de publicado. FlowField: type: object required: [key, label, type, required, options] properties: key: type: string description: Chave estável da resposta. label: { type: string } type: $ref: '#/components/schemas/FlowFieldType' required: { type: boolean } options: type: array items: type: object required: [id, title] properties: id: { type: string } title: { type: string } Flow: type: object required: [id, name, category, status, definition, responseCount] properties: id: type: string format: uuid description: Use como `flowId` no envio e no botão `FLOW` do template. name: { type: string } category: $ref: '#/components/schemas/FlowCategory' status: $ref: '#/components/schemas/FlowStatus' definition: type: object required: [title, submitLabel, fields] description: O formulário como foi criado, já com as chaves finais. properties: title: { type: string } intro: type: string nullable: true submitLabel: { type: string } fields: type: array items: $ref: '#/components/schemas/FlowField' responseCount: type: integer format: int64 description: Total de respostas recebidas. publishError: type: string nullable: true description: Preenchido só quando o formulário foi criado mas a publicação falhou. createdAt: type: string format: date-time nullable: true publishedAt: type: string format: date-time nullable: true example: id: 0b7e6a52-3c1d-4f7a-9a55-2f4f6f1f9c10 name: Captar contato category: LEAD_GENERATION status: PUBLISHED definition: title: Fale com a gente intro: Leva menos de um minuto. submitLabel: Enviar fields: - {key: nome, label: Seu nome, type: TEXT, required: true, options: []} - {key: email, label: Seu melhor e-mail, type: EMAIL, required: true, options: []} - key: interesse label: O que você procura? type: SELECT required: true options: - {id: falar_com_vendas, title: Falar com vendas} - {id: suporte, title: Suporte} responseCount: 42 publishError: null createdAt: '2026-09-15T14:30:00Z' publishedAt: '2026-09-15T14:30:04Z' FlowPage: type: object required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/Flow' pagination: $ref: '#/components/schemas/Pagination' FlowSubmission: type: object required: [id, customerNumber, answers] properties: id: { type: string, format: uuid } customerNumber: type: string description: Telefone de quem respondeu. answers: type: array items: type: object required: [key, label, value] properties: key: { type: string } label: { type: string } value: type: string description: Valor legível. Opção vem com o texto, múltipla escolha separada por vírgula, aceite como `Sim` ou `Não`, data como `dd/MM/yyyy`. createdAt: type: string format: date-time nullable: true example: id: 5f0c2a11-9d3e-4b7a-8c21-1a2b3c4d5e6f customerNumber: '5511999999999' answers: - {key: nome, label: Seu nome, value: Ana Exemplo} - {key: email, label: Seu melhor e-mail, value: ana@exemplo.com} createdAt: '2026-09-16T10:12:00Z' FlowSubmissionPage: type: object required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/FlowSubmission' pagination: $ref: '#/components/schemas/Pagination' # ---------- Automações ---------- AutomationStep: type: object required: [type] properties: type: type: string enum: [message, wait, condition, tag, question, wait_payment] description: | - `message`: envia um template (`templateId`, `variables` e, opcionalmente, `charge`) ou um texto na janela de 24h (`sessionText`, até 1.024 caracteres). Nunca os dois. - `wait`: `{ "minutes": N }`, de 1 a 43.200 (30 dias). - `condition`: `{ "tag": "..." }`. Segue só se o contato ainda não tem a etiqueta. - `tag`: `{ "tag": "..." }`. Aplica a etiqueta no contato. - `question`: pergunta com opções; ramifica em `branches`. - `wait_payment`: `{ "timeoutMinutes": N, "referenceId"?: "..." }`; ramifica em `paid` e `unpaid`. config: type: object nullable: true additionalProperties: true description: Configuração do passo. O formato depende de `type`. branches: type: object nullable: true description: | Só em `question` e `wait_payment`. Chave do ramo para a lista de passos daquele ramo. Em `question`: o `id` de cada opção, mais `__timeout` e `__other`. Em `wait_payment`: `paid` e `unpaid`. additionalProperties: type: array items: $ref: '#/components/schemas/AutomationStep' id: type: string format: uuid nullable: true readOnly: true description: ID do passo na resposta. Muda a cada `PUT`. AutomationUpsertRequest: type: object required: [name, trigger] properties: name: type: string description: Nome interno da automação. trigger: type: string enum: [webhook, cart.abandoned, payment.failed, tag.applied, conversation.started, button.replied, charge.paid, charge.expired] description: | Gatilho que coloca um contato na automação. `webhook` é o link próprio da automação; `cart.abandoned` e `payment.failed` vêm da recuperação de receita; os demais são eventos internos da Arara. triggerConfig: type: object nullable: true additionalProperties: true description: 'Só `button.replied` exige: `{ "replyId": "...", "templateName"?: "..." }`. Os demais gatilhos ignoram o campo.' steps: type: array default: [] description: Passos, na ordem de execução. A lista inteira é reescrita a cada `POST` ou `PUT`. items: $ref: '#/components/schemas/AutomationStep' AutomationHook: type: object required: [url, signatureEnabled, hasSecret] properties: url: type: string description: Link próprio da automação (`https://api.ararahq.com/hooks/a/ara_hook_...`). signatureEnabled: { type: boolean } hasSecret: type: boolean description: Diz só se o segredo existe. O valor nunca volta em leitura. AutomationHookSignature: type: object required: [url, signatureEnabled, hasSecret] properties: url: { type: string } signatureEnabled: { type: boolean } hasSecret: { type: boolean } secret: type: string nullable: true description: Segredo da assinatura. Vem preenchido uma única vez, na resposta de quem ligou. Automation: type: object required: [id, name, trigger, active, steps, costPerRunBrl] properties: id: { type: string, format: uuid } name: { type: string } trigger: { type: string } triggerConfig: type: object nullable: true additionalProperties: true active: type: boolean description: '`false` na criação. Mude em `PUT /v1/automations/{id}/active`.' steps: type: array items: $ref: '#/components/schemas/AutomationStep' costPerRunBrl: type: number description: Custo estimado, em reais, de cada pessoa que entrar na automação. stats: type: object nullable: true properties: runsLast30Days: { type: integer, format: int64 } inFlight: { type: integer, format: int64 } completed: { type: integer, format: int64 } stopped: { type: integer, format: int64 } webhookUrl: type: string nullable: true description: URL que o seu sistema precisa chamar, quando o gatilho depende disso. hook: allOf: - $ref: '#/components/schemas/AutomationHook' nullable: true description: Só no gatilho `webhook`. example: id: 3d5a1f0c-6b2e-4c8d-9a41-0e1f2a3b4c5d name: Lembrete de carrinho trigger: cart.abandoned triggerConfig: null active: false steps: - id: b2f0c1de-1a2b-4c3d-8e4f-5a6b7c8d9e0f type: wait config: { minutes: 60 } - id: c7a1e2d3-4b5c-4d6e-9f70-8a9b0c1d2e3f type: message config: templateId: 0b8f6c1e-7a52-4a1b-9a0e-3f2d6c9e1a44 variables: [nome] costPerRunBrl: 0.38 stats: { runsLast30Days: 0, inFlight: 0, completed: 0, stopped: 0 } webhookUrl: null hook: null AutomationCatalog: type: object required: [triggers, stepTypes] properties: triggers: type: array items: $ref: '#/components/schemas/AutomationCatalogEntry' stepTypes: type: array items: $ref: '#/components/schemas/AutomationCatalogEntry' AutomationCatalogEntry: type: object required: [value, internal] properties: value: type: string description: Valor aceito em `trigger` ou `steps[].type`. internal: type: boolean description: '`true` quando o evento nasce dentro da Arara, sem integração do seu lado.' AutomationTemplateOption: type: object required: [id, name, category, unitPriceBrl, hasChargeButton] properties: id: { type: string, format: uuid } name: { type: string } category: { type: string } unitPriceBrl: type: number description: Preço de referência do envio deste template, em reais. hasChargeButton: type: boolean description: Template de cobrança. O passo precisa informar a cobrança. AutomationRun: type: object required: [id, phoneNumber, status] properties: id: { type: string, format: uuid } phoneNumber: { type: string } status: type: string enum: [RUNNING, WAITING_REPLY, COMPLETED, STOPPED, FAILED] currentStepId: type: string format: uuid nullable: true createdAt: type: string format: date-time nullable: true finishedAt: type: string format: date-time nullable: true stoppedReason: type: string nullable: true AutomationRunPage: type: object required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/AutomationRun' pagination: $ref: '#/components/schemas/Pagination' AutomationRunEvent: type: object required: [id, kind, createdAt] properties: id: { type: string, format: uuid } stepId: type: string format: uuid nullable: true kind: type: string description: 'Tipo do evento, ex.: `STARTED`, `QUESTION_ASKED`, `REPLY_MATCHED`, `CHARGE_SENT`, `COMPLETED`.' detail: type: object nullable: true additionalProperties: true description: Muda conforme o `kind`. createdAt: { type: string, format: date-time } AutomationRunEventPage: type: object required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/AutomationRunEvent' pagination: $ref: '#/components/schemas/Pagination' HookStartRequest: type: object required: [phone] title: Iniciar execução properties: phone: type: string description: Telefone do cliente, com DDI. example: '5511999999999' variables: type: object nullable: true additionalProperties: { type: string } description: Variáveis da execução, usadas como `{{nome}}` nos passos. charge: type: object nullable: true additionalProperties: true description: | Cobrança que o passo de cobrança da automação vai enviar. `referenceId` é o seu número da cobrança: é com ele que você avisa o pagamento depois. Campos em [Link da automação e pagamento](/automacoes-webhook). HookPaymentUpdateRequest: type: object required: [referenceId, status] title: Atualizar pagamento properties: referenceId: type: string description: O mesmo `charge.referenceId` enviado ao iniciar. status: type: string description: 'Novo estado da cobrança, ex.: `paid`.' example: paid HookStarted: type: object required: [status] properties: runId: type: string format: uuid nullable: true status: type: string example: started HookPaymentUpdated: type: object required: [referenceId, status, cardUpdated, runResumed] properties: referenceId: { type: string } status: { type: string } cardUpdated: type: boolean description: '`true` quando a janela de 24h permitiu atualizar o cartão no WhatsApp.' runResumed: type: boolean description: '`true` quando havia uma execução esperando esse pagamento.'