🥑 Abacato AI

Documentação da API

API REST pública do Abacato AI para integrar seus próprios sistemas ao WhatsApp.

Com a API pública você envia mensagens de WhatsApp (texto, template e mídia), lista os contatos importados, consulta o status de entrega e gerencia templates — tudo autenticado por uma chave de API criada no painel. Também é possível receber eventos por webhook ou plugar um Chatwoot sem escrever código. A especificação legível por máquina está disponível em openapi.yaml.

Última atualização: 2026-07-15 · versão 1.3.0

Conteúdo

Primeiros passos

Base URL

https://api.abacato.vps.aprendendoai.com

Todas as requisições usam Content-Type: application/json e retornam JSON. Envios aceitos retornam 202; consultas retornam 200; uploads, 201.

Autenticação

Toda requisição é autenticada por uma chave de API enviada no cabeçalho X-API-Key. Crie e gerencie suas chaves no painel do Abacato AI em Configurações → Chaves de API. A chave completa é exibida uma única vez, no momento da criação — guarde-a com segurança.

X-API-Key: abct_live_xxxxxxxxxxxxxxxxxxxxxxxx

O cabeçalho Authorization: Bearer <chave> também é aceito, em qualquer endpoint — é o formato que clientes da Meta Cloud API já enviam.

Nunca exponha a chave no front-end. A chave dá acesso ao envio de mensagens em nome da sua conta. Use-a apenas a partir do seu servidor.

Referência de endpoints

Todos os caminhos são relativos à Base URL e ficam sob /api/v1/public. Todos exigem o cabeçalho X-API-Key.

MétodoCaminhoDescrição
POST/api/v1/public/messages/textEnvia uma mensagem de texto
POST/api/v1/public/messages/templateEnvia uma mensagem de template aprovado
POST/api/v1/public/messages/mediaEnvia imagem, vídeo, áudio, documento ou figurinha
GET/api/v1/public/contactsLista os contatos importados
GET/api/v1/public/whatsapp/statusConsulta o status da conta de WhatsApp
GET/api/v1/public/messages/{id}Consulta o status de entrega de uma mensagem
GET/api/v1/public/messagesLista as mensagens recentes
GET/api/v1/public/numbersLista os números de WhatsApp conectados
GET/api/v1/public/templatesLista os templates
POST/api/v1/public/templatesCria um template
POST/api/v1/public/templates/{id}Edita um template (pelo id da Meta)
DELETE/api/v1/public/templates/{name}Exclui um template (pelo nome)
POST/api/v1/public/mediaFaz upload de um arquivo e devolve um id de mídia
GET/api/v1/public/media/{mediaId}Baixa os bytes de uma mídia
DELETE/api/v1/public/media/{mediaId}Exclui uma mídia
Envio aceito ≠ mensagem entregue. Os endpoints de envio (texto, template e mídia) retornam 202 Accepted: a Meta apenas aceitou a mensagem. A entrega é assíncrona — use Status da mensagem para saber se virou delivered ou failed.

Enviar uma mensagem de texto

POST /api/v1/public/messages/text
Só dentro da janela de 24 horas. Pela política da Meta, mensagens de texto livre só podem ser enviadas dentro da janela de atendimento de 24 horas — ou seja, até 24h após a última mensagem que o cliente enviou para o seu número. Fora dessa janela, a API retorna 422 com uma mensagem indicando que a janela está fechada. Para iniciar ou reabrir uma conversa, use um template aprovado, que pode ser enviado a qualquer momento.

Parâmetros do corpo

CampoTipoObrigatórioDescrição
tostringsimNúmero do destinatário, apenas dígitos (E.164). Ex.: 5511888888888
textstringsimCorpo da mensagem, até 4096 caracteres

Requisição

# envia um texto simples
curl -X POST https://api.abacato.vps.aprendendoai.com/api/v1/public/messages/text \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "5511888888888",
    "from": "5511987654321",
    "text": "Olá! Essa é uma mensagem de teste."
  }'

from (o número de origem, só dígitos) é opcional — necessário só se você tem vários números e quer escolher qual envia. Com um número, pode omitir.

Resposta 202 — aceita pela Meta (ainda não entregue)

{
  "message": {
    "id": "9f1c2d3e-...",
    "direction": "outbound",
    "recipientNumber": "5511888888888",
    "messageType": "text",
    "content": "Olá! Essa é uma mensagem de teste.",
    "wamid": "wamid.HBg...",
    "status": "sent",
    "sentAt": "2026-07-08T14:00:00.000Z"
  }
}

Enviar uma mensagem de template

POST /api/v1/public/messages/template

Templates podem ser enviados a qualquer momento (fora da janela de 24h de atendimento). O template precisa já estar aprovado na conta de WhatsApp Business.

Somente contas via API Oficial (Meta Cloud). Contas conectadas por QR Code (Evolution) não enviam templates — use Enviar texto.

Parâmetros do corpo

CampoTipoObrigatórioDescrição
tostringsimNúmero do destinatário, apenas dígitos (E.164)
templateNamestringsimNome do template aprovado. Ex.: pedido_atualizado
languagestringnãoCódigo de idioma do template (padrão pt_BR)
componentsarraynãoComponentes para preencher variáveis (formato Meta): body para o corpo e button (com sub_type e index) para botão de URL dinâmica. Não use em templates de Authentication.
codestringsó em AuthenticationCódigo de uso único gerado pelo seu sistema
from / accountIdstringnãoDe qual número enviar (seletor). Com vários números, o padrão é o conectado há mais tempo.
Com mais de um número, escolha de qual enviar. Se você omitir from/ accountId, o envio sai pelo número conectado há mais tempo (o primário). Como um template só existe na WABA onde foi criado, mande pelo número certo — use from (o número, só dígitos) ou accountId (o id de /numbers).
Templates de Authentication (OTP). A Meta não gera o código — quem gera é o seu sistema. Envie apenas o campo code: a API monta sozinha os dois componentes que a Meta exige (o corpo, com o {{1}}, e o botão "Copiar código"). Mandar só components com o corpo resulta no erro (#131008) … Button at index 0 of type Url requires a parameter. Se o envio for recusado, o código não se perde: ele fica visível no painel por 24 horas, só para o dono da conta (Histórico de mensagens → Ver tentativa).

Requisição

curl -X POST https://api.abacato.vps.aprendendoai.com/api/v1/public/messages/template \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "5511888888888",
    "from": "5511987654321",
    "templateName": "pedido_atualizado",
    "language": "pt_BR",
    "components": [
      { "type": "body",
        "parameters": [ { "type": "text", "text": "#1234" } ] }
    ]
  }'

from pode ser omitido se você tem um único número conectado.

Requisição — botão de URL dinâmica

Só o fim da URL é dinâmico. A base (domínio + caminho) é fixada quando o template é aprovado; a variável do botão só substitui o {{1}} no fim da URL. Se o botão foi criado como https://pay.suaempresa.com/{{1}}, envie apenas o trecho final (ex.: abc123) e a Meta monta https://pay.suaempresa.com/abc123. Deixe o que varia por cliente num único token no fim (id ou slug do pagamento); partes fixas — inclusive query — ficam na base do template.

O botão vira um componente próprio, com sub_type: "url" e index: "0" (o índice do botão no template). Se o template também tem variáveis no corpo, mande os dois componentes:

curl -X POST https://api.abacato.vps.aprendendoai.com/api/v1/public/messages/template \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "5511888888888",
    "templateName": "enviar_fatura",
    "language": "pt_BR",
    "components": [
      { "type": "body",
        "parameters": [ { "type": "text", "text": "Charles" } ] },
      { "type": "button", "sub_type": "url", "index": "0",
        "parameters": [ { "type": "text", "text": "9f8e7d" } ] }
    ]
  }'
Omitir o componente button num template com botão de URL dinâmica resulta em (#131008) … Button at index 0 of type Url requires a parameter.

Requisição — template de Authentication (OTP)

# o código é gerado pelo seu sistema; a API monta corpo + botão
curl -X POST https://api.abacato.vps.aprendendoai.com/api/v1/public/messages/template \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "5511888888888",
    "templateName": "codigo_acesso",
    "language": "pt_BR",
    "code": "123456"
  }'

Requisição — cabeçalho de mídia (vídeo, imagem ou documento)

Se o template foi criado com cabeçalho de mídia, informe o arquivo real num componente header. A mídia entra por link (URL pública) ou por id (de um upload, reutilizável por ~30 dias):

curl -X POST https://api.abacato.vps.aprendendoai.com/api/v1/public/messages/template \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "5511888888888",
    "templateName": "conta_criada_teste_30_dias",
    "language": "pt_BR",
    "components": [
      { "type": "header",
        "parameters": [
          { "type": "video", "video": { "link": "https://exemplo.co/videos/instalacao.mp4" } }
        ] },
      { "type": "body",
        "parameters": [ { "type": "text", "text": "João" } ] }
    ]
  }'
Troque "video" por "image" ou "document" conforme o headerFormat do template, e link por { "id": "<mediaId>" } se preferir enviar por upload. A mídia do cabeçalho é obrigatória em todo envio — mesmo que seja sempre a mesma, a Meta não a "fixa" na aprovação.

Resposta 202 — aceita pela Meta (ainda não entregue)

{
  "message": {
    "id": "9f1c2d3e-...",
    "direction": "outbound",
    "recipientNumber": "5511888888888",
    "messageType": "template",
    "templateName": "pedido_atualizado",
    "wamid": "wamid.HBg...",
    "status": "sent",
    "sentAt": "2026-07-08T14:00:00.000Z"
  }
}

Listar contatos importados

GET /api/v1/public/contacts

Parâmetros de query

ParâmetroTipoPadrãoDescrição
pageinteger1Página da listagem
limitinteger50Itens por página (máx. 100)
searchstringFiltra por nome ou número, por correspondência parcial e sem diferenciar maiúsculas
fromstringDe qual número conectado listar os contatos (seletor)
accountIdstringIdem, pelo id devolvido em /numbers. Tem precedência sobre from

Os contatos pertencem a um número. Sem from nem accountId, listamos os do número conectado há mais tempo (o primário) — o mesmo padrão do envio.

Requisição

# Primeira página, padrão (50 itens do número mais recente)
curl "https://api.abacato.vps.aprendendoai.com/api/v1/public/contacts" \
  -H "X-API-Key: abct_live_xxx"

# Segunda página, 20 por vez
curl "https://api.abacato.vps.aprendendoai.com/api/v1/public/contacts?page=2&limit=20" \
  -H "X-API-Key: abct_live_xxx"

# Buscar por nome ou trecho do número
curl "https://api.abacato.vps.aprendendoai.com/api/v1/public/contacts?search=maria" \
  -H "X-API-Key: abct_live_xxx"

# Contatos de um número específico, com busca. Codifique o valor na URL:
# um espaco vira %20 e o + de um telefone vira %2B
curl "https://api.abacato.vps.aprendendoai.com/api/v1/public/contacts?from=5511999999999&search=maria%20silva" \
  -H "X-API-Key: abct_live_xxx"

Resposta 200

{
  "contacts": [
    {
      "id": "3a2b1c...",
      "phoneNumber": "5511888888888",
      "name": "Maria Silva",
      "createdAt": "2026-07-01T10:00:00.000Z",
      "updatedAt": "2026-07-01T10:00:00.000Z"
    }
  ],
  "total": 128,
  "page": 1,
  "limit": 50
}

Status da conta de WhatsApp

GET /api/v1/public/whatsapp/status

Retorna o número conectado há mais tempo (o primário) — o mesmo pelo qual saem os envios sem seletor —, ou account: null se nenhum estiver conectado. Com vários números conectados, use GET /numbers para ver todos.

Requisição

curl https://api.abacato.vps.aprendendoai.com/api/v1/public/whatsapp/status \
  -H "X-API-Key: abct_live_xxx"

Resposta 200

{
  "account": {
    "id": "2e4ff797-b858-4676-bf18-355997675a36",
    "provider": "meta_cloud",
    "wabaId": "1234567890",
    "phoneNumberId": "9876543210",
    "evolutionInstanceName": null,
    "displayName": "Minha Empresa",
    "displayPhoneNumber": "+55 11 98765-4321",
    "label": "Atendimento",
    "qualityRating": "GREEN",
    "status": "active",
    "isCoex": true,
    "connectedAt": "2026-07-08T19:17:03.783Z"
  }
}

evolutionInstanceName só vem preenchido em conexões via QR Code; wabaId e phoneNumberId, só na API Oficial. label é o nome que você deu ao número no painel, ou null.

Listar mensagens

GET /api/v1/public/messages

Mensagens recentes da conta, da mais nova para a mais antiga.

Parâmetros de query

ParâmetroTipoPadrãoDescrição
limitinteger20Itens por página (máx. 100)
beforestringCursor ISO 8601. Passe o sentAt da mensagem mais antiga que você já tem para pegar a próxima página.
typestringFiltra por tipo (text, template, …)
statusstringFiltra por status (sent, delivered, read, failed)
fromstringDe qual número conectado ler o histórico (seletor)
accountIdstringIdem, pelo id de /numbers. Tem precedência sobre from

As mensagens pertencem a um número. Sem from nem accountId, listamos as do número conectado há mais tempo (o primário).

Requisição

curl "https://api.abacato.vps.aprendendoai.com/api/v1/public/messages?limit=20&status=failed" \
  -H "X-API-Key: abct_live_xxx"

Resposta 200

{
  "messages": [ /* mesmo formato de Status da mensagem */ ],
  "hasMore": true
}

Enviar mídia

POST /api/v1/public/messages/media
Também exige a janela de 24 horas, igual à mensagem de texto.

Envie exatamente um entre link (URL pública) ou mediaId (de um upload anterior). Enviar os dois, ou nenhum, retorna 400.

Corpo

CampoTipoDescrição
tostringNúmero do destinatário, com código do país
typestringimage, video, audio, document ou sticker
linkstringURL pública do arquivo
mediaIdstringId devolvido por POST /media
captionstringLegenda (image, video e document)
filenamestringNome exibido do arquivo (document)

Requisição

curl -X POST "https://api.abacato.vps.aprendendoai.com/api/v1/public/messages/media" \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"to":"5511999999999","from":"5511987654321","type":"image","link":"https://exemplo.com/foto.jpg","caption":"Olá!"}'

from/accountId opcional — ver Escolher o número de envio.

Resposta 202

{
  "message": {
    "id": "9f8e7d...",
    "messageType": "image",
    "wamid": "wamid.HBgM...",
    "status": "sent"
  }
}

Números conectados

GET /api/v1/public/numbers

Todos os números de WhatsApp conectados à sua conta.

Resposta 200

{
  "numbers": [
    {
      "id": "7d6e5f...",
      "provider": "meta_cloud",
      "wabaId": "1234567890",
      "phoneNumberId": "9876543210",
      "displayName": "Minha Empresa",
      "displayPhoneNumber": "+55 11 99999-9999",
      "label": "Suporte",
      "qualityRating": "GREEN",
      "status": "active",
      "connectedAt": "2026-07-08T12:00:00.000Z"
    }
  ]
}

label é o nome que você deu ao número no painel. status pode vir como in_review: é a janela de 24-48h em que a Meta analisa um número recém-conectado — ele já envia e recebe normalmente.

Escolher o número de envio

Com mais de um número conectado, todo endpoint de envio e de consulta aceita um seletor opcional. A precedência é accountId, depois from, e por fim — se você não passar nenhum — o número conectado há mais tempo (o seu número primário).

CampoO que éOnde encontrar
fromO número de origem, só dígitos, no formato E.164 (ex.: 5511987654321)É o próprio número de telefone que aparece em Conectar WhatsApp, sem +, espaços ou traços. O jeito mais simples.
accountIdO identificador interno da conexão (um UUID, ex.: 2e4ff797-b858-4676-bf18-355997675a36)É o campo id de cada item em GET /numbers. Para uso programático.
Não confunda: accountId não é o WABA ID (esse é o wabaId, da conta comercial) nem o nome/rótulo do número. Na dúvida, use from com o próprio número de telefone — é o mais fácil.
curl -X POST "https://api.abacato.vps.aprendendoai.com/api/v1/public/messages/text" \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"from":"5511987654321","to":"5511888888888","text":"Olá"}'
Com um único número conectado, pode ignorar o seletor — ele é resolvido sozinho. Com vários e sem seletor, o envio sai pelo número conectado há mais tempo; para escolher outro, passe from ou accountId.

Templates

Só em contas via API Oficial (Meta Cloud). Criar ou editar um template o envia para revisão da Meta: ele fica PENDING e só pode ser enviado depois de APPROVED. Se for reprovado, o motivo vem em rejectedReason.

Listar templates

GET /api/v1/public/templates

Retorna todos os templates da conta, já com o texto do corpo e os componentes da Meta — é daqui que você lê as variáveis de cada template.

Resposta 200

{
  "templates": [
    {
      "id": "1a2b3c...",
      "metaTemplateId": "1234567890",
      "name": "pedido_atualizado",
      "category": "UTILITY",
      "status": "APPROVED",
      "language": "pt_BR",
      "bodyText": "Olá {{1}}, sua fatura {{2}} está disponível.",
      "components": [
        {
          "type": "BODY",
          "text": "Olá {{1}}, sua fatura {{2}} está disponível.",
          "example": { "body_text": [[ "João", "#1234" ]] }
        },
        {
          "type": "BUTTONS",
          "buttons": [
            { "type": "URL", "text": "Ver fatura", "url": "https://exemplo.co/f?id={{1}}", "example": [ "https://exemplo.co/f?id=9f8e7d" ] }
          ]
        }
      ],
      "pendingCategory": null,
      "rejectedReason": null
    }
  ]
}

Consultar as variáveis de um template

As variáveis ficam em dois lugares da resposta acima:

A variável do botão é separada da do corpo. Na Meta, a variável de um botão de URL é sempre {{1}} dentro do botão, num componente à parte — independente do corpo. Por isso um template pode ter {{1}} no BODY e {{1}} no botão ao mesmo tempo: são variáveis diferentes, preenchidas por componentes diferentes no envio.

Criar template

POST /api/v1/public/templates

Parâmetros do corpo

CampoTipoObrigatórioDescrição
namestringsimlowercase_with_underscores
categorystringsimMARKETING, UTILITY ou AUTHENTICATION
languagestringnãoPadrão pt_BR
bodyTextstringsim*Corpo, com variáveis {{1}} ou {{nome}}. *Não se aplica a AUTHENTICATION.
headerText / footerTextstringnãoCabeçalho (texto) e rodapé. Para cabeçalho de mídia, use headerFormat abaixo.
headerFormatstringnãoTipo do cabeçalho: TEXT (padrão), IMAGE, VIDEO ou DOCUMENT. Para os três de mídia, informe também headerMediaUrl (ou headerHandle).
headerMediaUrlstringcom header de mídiaURL pública https do arquivo de exemplo (imagem/vídeo/documento). Nós baixamos e enviamos à Meta para a aprovação.
headerHandlestringnãoAlternativa avançada ao headerMediaUrl: um handle de mídia já enviado à Meta. Se informado, tem precedência.
buttonsarraynãoAté 10. Tipos abaixo.
parameterFormatstringnãoPOSITIONAL ({{1}}, padrão) ou NAMED ({{nome}}). Não misture os dois.
messageSendTtlSecondsintegernãoValidade da mensagem. Utility 30–43200 (12h); Marketing 43200–2592000 (30d); Auth 30–900 (15min). -1 = 30 dias (exceto Marketing).
variableExamplesobjectnãoExemplo por variável, pela chave como escrita: {"1": "João"} ou {"nome": "João"}
codeExpirationMinutesintegernãoSó Authentication. 1–90, padrão 10.
addSecurityRecommendationbooleannãoSó Authentication. Padrão true.
otpButtonTextstringnãoSó Authentication. Padrão "Copiar código".

Tipos de botão

typeCamposDescrição
QUICK_REPLYtextResposta rápida
URLtext, url, urlExampleLink. No máximo uma variável, sempre no fim da URL (ex.: .../fatura?id={{1}}); urlExample é só o valor de amostra (a parte depois da base). É a variável própria do botão — sempre {{1}} na Meta, separada das do corpo; um {{2}} que você escrever aqui é normalizado para {{1}}.
PHONE_NUMBERtext, phoneNumberBotão de ligar
COPY_CODEexampleCopiar código/cupom (Marketing/Utility). Sem text — o WhatsApp rotula sozinho; example é um código de amostra.
O botão OTP não entra nesta lista. Ele é exclusivo dos templates de Authentication e você não o escolhe no array buttons. Para um template OTP, basta criar com "category": "AUTHENTICATION": a API monta sozinha o botão de copiar o código, e você só ajusta o rótulo dele em otpButtonText (padrão "Copiar código"). Não confunda com o COPY_CODE acima, que é um botão de cupom para templates de Marketing/Utility — apesar do rótulo parecido, é outra coisa.
Authentication é montado pela API. Não envie bodyText, headerText, footerText nem buttons — a Meta impõe a estrutura (corpo gerado + botão OTP). Use apenas codeExpirationMinutes, addSecurityRecommendation e otpButtonText.

Requisição

curl -X POST https://api.abacato.vps.aprendendoai.com/api/v1/public/templates \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "pedido_atualizado",
    "category": "UTILITY",
    "language": "pt_BR",
    "bodyText": "Olá {{1}}, sua fatura {{2}} está disponível.",
    "footerText": "Toque no botão para pagar",
    "variableExamples": { "1": "João", "2": "#1234" },
    "messageSendTtlSeconds": 43200,
    "buttons": [
      { "type": "URL", "text": "Ver fatura", "url": "https://exemplo.co/f?id={{1}}", "urlExample": "9f8e7d" }
    ]
  }'

Retorna 201. O status e o category vêm da resposta da Meta — um template enviado como UTILITY pode voltar APPROVED como MARKETING se o conteúdo for promocional (custa ~9x mais).

Exemplo com variáveis nomeadas. Envie "parameterFormat": "NAMED" e use {{nome_cliente}} no texto, com variableExamples pela mesma chave: { "nome_cliente": "João", "numero_fatura": "#1234" }. Não misture {{1}} e {{nome}} no mesmo template.

Cabeçalho de mídia (imagem, vídeo ou documento)

Um template pode exibir uma imagem, vídeo ou documento acima do corpo — é o cabeçalho de mídia (não é um botão; botões não reproduzem mídia). Informe headerFormat e a URL pública do arquivo de exemplo em headerMediaUrl:

curl -X POST https://api.abacato.vps.aprendendoai.com/api/v1/public/templates \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "conta_criada_teste_30_dias",
    "category": "UTILITY",
    "language": "pt_BR",
    "headerFormat": "VIDEO",
    "headerMediaUrl": "https://exemplo.co/videos/instalacao.mp4",
    "bodyText": "Olá {{1}}! Sua conta foi criada e o teste de 30 dias já está ativo.",
    "footerText": "Atendimento de seg a sex, das 9h às 18h.",
    "variableExamples": { "1": "João" },
    "buttons": [
      { "type": "URL", "text": "Acessar o Abacato", "url": "https://abacato.app" }
    ]
  }'
O arquivo da criação é só o exemplo de aprovação. A Meta usa esse vídeo/imagem para revisar o template. Na hora de enviar, você informa a mídia real num componente header — inclusive um arquivo diferente por disparo. Veja enviar template com cabeçalho de mídia.

Limites da Meta: imagem jpeg/png ≤ 5 MB · vídeo mp4 (H.264+AAC) ≤ 16 MB · documento pdf ≤ 100 MB. Alternativamente, em vez de headerMediaUrl você pode passar um headerHandle de mídia já enviado à Meta.

Editar template

POST /api/v1/public/templates/{id}

O {id} é o metaTemplateId (o id da Meta), não o id interno. Nome e idioma não podem mudar. Aceita os mesmos campos de conteúdo do criar. Depois da edição a Meta re-revisa e o status volta para PENDING.

Requisição

curl -X POST https://api.abacato.vps.aprendendoai.com/api/v1/public/templates/1234567890 \
  -H "X-API-Key: abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "bodyText": "Olá {{1}}, seu pedido {{2}} saiu para entrega.",
    "variableExamples": { "1": "João", "2": "#1234" }
  }'

Excluir template

DELETE /api/v1/public/templates/{name}

A Meta exclui templates pelo nome, não pelo id.

Requisição

curl -X DELETE https://api.abacato.vps.aprendendoai.com/api/v1/public/templates/pedido_atualizado \
  -H "X-API-Key: abct_live_xxx"

Resposta 200

{ "deleted": true }

Status da mensagem

GET /api/v1/public/messages/{id}

O {id} é o message.id (UUID) devolvido no envio. Como a entrega é assíncrona, este é o endpoint que diz se a mensagem realmente chegou.

sent não é entrega. Ele significa apenas que a Meta aceitou a mensagem e devolveu um wamid. Só considere sucesso quando o status for delivered (ou read).

Ciclo de vida do status

StatusSignificado
sentA Meta aceitou. Ainda não foi entregue.
deliveredEntregue no aparelho do destinatário.
readLida pelo destinatário.
failedNão foi entregue. O motivo vem em errorMessage.

Requisição

curl https://api.abacato.vps.aprendendoai.com/api/v1/public/messages/9f1c2d3e-... \
  -H "X-API-Key: abct_live_xxx"

Resposta 200 — exemplo de falha na entrega

{
  "message": {
    "id": "9f1c2d3e-...",
    "recipientNumber": "5511888888888",
    "messageType": "template",
    "templateName": "codigo_acesso",
    "wamid": "wamid.HBg...",
    "status": "failed",
    "errorMessage": "Business eligibility payment issue",
    "sentAt": "2026-07-09T13:20:07.787Z",
    "statusUpdatedAt": "2026-07-09T13:20:09.013Z"
  }
}

Um 404 significa que a mensagem não existe ou não pertence à sua conta.

Mídia

Para enviar um arquivo que não está numa URL pública, faça upload primeiro e use o mediaId devolvido em POST /messages/media.

Upload

POST /api/v1/public/media

Requisição multipart/form-data com o campo file. Limite de 100 MB; acima disso a resposta é 413.

curl -X POST "https://api.abacato.vps.aprendendoai.com/api/v1/public/media" \
  -H "X-API-Key: abct_live_xxx" \
  -F "file=@/caminho/foto.jpg"

Resposta 201

{
  "id": "1234567890",
  "mimeType": "image/jpeg",
  "fileSize": 83122,
  "filename": "foto.jpg"
}

O id é o que você passa como mediaId em POST /messages/media. Ele é reutilizável por 30 dias.

Download

GET /api/v1/public/media/{mediaId}

Devolve os bytes do arquivo, com o Content-Type original. Use este endpoint para baixar mídias recebidas em mensagens: a URL que a Meta gera expira em minutos e só funciona com o nosso token, então nunca é exposta.

Exclusão

DELETE /api/v1/public/media/{mediaId}

Resposta 200

{ "deleted": true }

Webhooks

Configure em Configurações → Webhooks uma URL https para receber mensagens recebidas e atualizações de status. É a forma recomendada de saber que uma mensagem foi entregue, em vez de consultar o status em loop.

Assinatura

Todo POST leva o cabeçalho X-Abacato-Signature, no formato sha256=<hmac>, calculado sobre o corpo bruto com o seu segredo de assinatura. Verifique-o antes de confiar no evento.

// Node.js
const esperado = 'sha256=' + crypto
  .createHmac('sha256', segredo)
  .update(corpoBruto)
  .digest('hex');
const valido = crypto.timingSafeEqual(
  Buffer.from(esperado), Buffer.from(req.headers['x-abacato-signature'])
);

Evento: mensagem recebida

{
  "event": "message.received",
  "timestamp": "2026-07-09T12:00:00.000Z",
  "data": {
    "from": "5511888888888",
    "contact_name": "Ana",
    "message_id": "wamid.HBgM...",
    "type": "image",
    "image": {
      "id": "1234567890",
      "media_url": "https://api.abacato.vps.aprendendoai.com/api/v1/public/media/1234567890"
    }
  },
  "account": { "phone_number_id": "9876543210" }
}

Em mídias adicionamos media_url: um link pronto para baixar os bytes com a sua chave de API, já que o id da Meta sozinho não é resolvível por você.

Evento: status da mensagem

{
  "event": "message.status",
  "data": {
    "message_id": "wamid.HBgM...",
    "status": "delivered",
    "recipient_id": "5511888888888"
  }
}

Entrega e reenvio

Responda 2xx rapidamente. Falhas são repetidas duas vezes (+30s e +120s) e registradas em Entregas recentes, de onde você pode reenviar manualmente — um reenvio traz o cabeçalho X-Abacato-Redelivery.

Entrega idempotente. A Meta reenvia webhooks com frequência. Deduplicamos por evento antes de chamar sua URL, então o mesmo message_id não chega duas vezes.

Integração com Chatwoot

O Chatwoot já traz um canal nativo de WhatsApp Cloud. Como falamos o protocolo da Meta, basta apontá-lo para nós: nenhuma alteração de código.

  1. Em Configurações → Webhooks, no cartão Chatwoot, informe a URL base do seu Chatwoot e salve.
  2. Copie o bloco de variáveis exibido e reinicie o Chatwoot:
WHATSAPP_CLOUD_BASE_URL=https://api.abacato.vps.aprendendoai.com
WHATSAPP_APP_SECRET=whsec_xxxxxxxxxxxxxxxx
Defina as variáveis nos dois serviços do Chatwoot. Se o seu Chatwoot roda o web e o worker (Sidekiq) separados, coloque as variáveis nos dois e reinicie cada um. O envio de mensagens passa pelo Sidekiq — sem a variável lá, ele manda para a URL padrão da Meta com a sua chave do Abacato e falha com Authentication Error.
  1. Crie uma caixa de entrada WhatsApp com provedor WhatsApp Cloud, usando o phoneNumberId e o wabaId mostrados no painel. Como token de acesso, use uma chave de API do Abacato.

A partir daí, mensagens recebidas caem na caixa de entrada e as respostas dos agentes saem pelo seu número, sem intermediários.

O Chatwoot e o endpoint próprio convivem. As duas integrações são independentes: você pode manter um endpoint próprio recebendo os eventos (por exemplo, para automações) e, ao mesmo tempo, o Chatwoot para o atendimento humano — no mesmo número. Cada um tem sua própria URL e seu próprio secret.
Tudo o que você envia pela API também aparece. Mensagens enviadas pelos endpoints de envio — texto, template (renderizado com as variáveis preenchidas) e mídia — são espelhadas no Chatwoot como mensagens enviadas, na conversa certa. Assim toda a conversa do número fica visível para o atendimento, sem depender de quem disparou a mensagem.

Respostas enviadas pelo celular também aparecem. No modo Coexistência, o que um atendente digita no app do WhatsApp Business chega à Meta como eco. Nós o encaminhamos no campo smb_message_echoes, que o Chatwoot grava como mensagem enviada na conversa certa — o seu Chatwoot precisa ser uma versão com suporte a Coexistência.

A exceção é o WhatsApp para Windows e o WearOS: a Meta não emite eco para esses aplicativos, então mensagens enviadas por eles não chegam a lugar nenhum — nem ao Chatwoot, nem à API.

Compatibilidade Meta Cloud API

Qualquer cliente que já fale a Graph API da Meta pode apontar a base URL para nós. Autentique com a sua chave de API, tanto em X-API-Key quanto em Authorization: Bearer.

MétodoCaminhoDescrição
POST/{versão}/{phone_number_id}/messagesTexto, template, mídia e confirmação de leitura
POST/{versão}/{phone_number_id}/mediaUpload (multipart)
GET/{versão}/{waba_id}/message_templatesLista os templates
GET/{versão}/{waba_id}/phone_numbersLista os números da conta (usado pela criação de caixa de entrada no Chatwoot)
GET/{versão}/{media_id}Metadados da mídia

Qualquer versão no formato v25.0 é aceita. As respostas — inclusive os erros — usam o mesmo envelope da Meta. Em GET /{media_id}, o campo url aponta para o nosso download: a URL da Meta expira e exige o nosso token.

curl -X POST "https://api.abacato.vps.aprendendoai.com/v25.0/9876543210/messages" \
  -H "Authorization: Bearer abct_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"messaging_product":"whatsapp","to":"5511999999999","type":"text","text":{"body":"Olá"}}'

Limites de taxa

Cada chave de API pode fazer até 1000 requisições a cada 15 minutos. Ao exceder, a API retorna 429 Too Many Requests — aguarde e tente novamente.

Erros

Erros retornam um corpo JSON no formato { "error": "descrição" }.

StatusSignificadoQuando acontece
400Bad RequestCorpo inválido, campos obrigatórios ausentes, ou envio recusado (ex.: destinatário igual ao número remetente)
401UnauthorizedChave de API ausente, inválida ou inativa
404Not FoundRecurso inexistente ou que não pertence à sua conta (ex.: mensagem por id)
413Payload Too LargeUpload de mídia acima de 100 MB
422Unprocessable EntityEnvio recusado pela regra da Meta — ex.: janela de 24h fechada
429Too Many RequestsLimite de taxa excedido (1000 req / 15 min por chave)
{
  "error": "Invalid or inactive API key"
}

Envio recusado

Quando um envio de texto, template ou mídia é recusado (pela Meta, ou pela API antes de chegar nela), a tentativa fica registrada como uma mensagem com status failed, e a resposta de erro traz o messageId dela. Consulte-a em Status da mensagem: o errorMessage repete o motivo. Um corpo inválido (um campo obrigatório ausente, por exemplo) não gera registro nem messageId.

{
  "error": "Cannot send a message to the sending number itself. Use a different recipient, or send from another connected number.",
  "messageId": "4b7e0c1a-..."
}
O destinatário não pode ser o próprio número remetente. A Meta recusa esse envio com um genérico (#100) Invalid parameter. A API detecta o caso antes e responde 400 com a mensagem acima, comparando os números com e sem o nono dígito.