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
- Primeiros passos
- Autenticação
- Referência de endpoints
- Enviar texto
- Enviar template
- Listar contatos
- Status do WhatsApp
- Listar mensagens
- Enviar mídia
- Números conectados
- Escolher o número de envio
- Templates
- Mídia
- Status da mensagem
- Webhooks
- Integração com Chatwoot
- Compatibilidade Meta Cloud API
- Limites de taxa
- Erros
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.
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étodo | Caminho | Descrição |
|---|---|---|
| POST | /api/v1/public/messages/text | Envia uma mensagem de texto |
| POST | /api/v1/public/messages/template | Envia uma mensagem de template aprovado |
| POST | /api/v1/public/messages/media | Envia imagem, vídeo, áudio, documento ou figurinha |
| GET | /api/v1/public/contacts | Lista os contatos importados |
| GET | /api/v1/public/whatsapp/status | Consulta o status da conta de WhatsApp |
| GET | /api/v1/public/messages/{id} | Consulta o status de entrega de uma mensagem |
| GET | /api/v1/public/messages | Lista as mensagens recentes |
| GET | /api/v1/public/numbers | Lista os números de WhatsApp conectados |
| GET | /api/v1/public/templates | Lista os templates |
| POST | /api/v1/public/templates | Cria 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/media | Faz 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 |
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
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
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Número do destinatário, apenas dígitos (E.164). Ex.: 5511888888888 |
text | string | sim | Corpo 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
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.
Parâmetros do corpo
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Número do destinatário, apenas dígitos (E.164) |
templateName | string | sim | Nome do template aprovado. Ex.: pedido_atualizado |
language | string | não | Código de idioma do template (padrão pt_BR) |
components | array | não | Componentes 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. |
code | string | só em Authentication | Código de uso único gerado pelo seu sistema |
from / accountId | string | não | De qual número enviar (seletor). Com vários números, o padrão é o conectado há mais tempo. |
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).
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
{{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" } ] }
]
}'
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" } ] }
]
}'
"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
Parâmetros de query
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
page | integer | 1 | Página da listagem |
limit | integer | 50 | Itens por página (máx. 100) |
search | string | — | Filtra por nome ou número, por correspondência parcial e sem diferenciar maiúsculas |
from | string | — | De qual número conectado listar os contatos (seletor) |
accountId | string | — | Idem, 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
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
Mensagens recentes da conta, da mais nova para a mais antiga.
Parâmetros de query
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
limit | integer | 20 | Itens por página (máx. 100) |
before | string | — | Cursor ISO 8601. Passe o sentAt da mensagem mais antiga que você já tem para pegar a próxima página. |
type | string | — | Filtra por tipo (text, template, …) |
status | string | — | Filtra por status (sent, delivered, read, failed) |
from | string | — | De qual número conectado ler o histórico (seletor) |
accountId | string | — | Idem, 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
Envie exatamente um entre link (URL pública) ou mediaId
(de um upload anterior). Enviar os dois, ou nenhum, retorna 400.
Corpo
| Campo | Tipo | Descrição |
|---|---|---|
to | string | Número do destinatário, com código do país |
type | string | image, video, audio, document ou sticker |
link | string | URL pública do arquivo |
mediaId | string | Id devolvido por POST /media |
caption | string | Legenda (image, video e document) |
filename | string | Nome 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
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).
| Campo | O que é | Onde encontrar |
|---|---|---|
from | O 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. |
accountId | O 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. |
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á"}'
from ou accountId.
Templates
PENDING e só pode ser enviado depois
de APPROVED. Se for reprovado, o motivo vem em rejectedReason.
Listar 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:
bodyTextmostra os espaços reservados como estão no texto:{{1}},{{2}}(posicionais) ou{{nome}}(nomeadas).componentscarrega os valores de exemplo aprovados:- No componente
BODY, posicionais vêm emexample.body_text— um array de linhas:[[ "João", "#1234" ]](o 1º valor é{{1}}, o 2º é{{2}}). Nomeadas vêm emexample.body_text_named_params:[{ "param_name": "nome", "example": "João" }]. - No botão
URL, o exemplo é a URL já preenchida emexample:["https://exemplo.co/f?id=9f8e7d"].
- No componente
{{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
Parâmetros do corpo
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string | sim | lowercase_with_underscores |
category | string | sim | MARKETING, UTILITY ou AUTHENTICATION |
language | string | não | Padrão pt_BR |
bodyText | string | sim* | Corpo, com variáveis {{1}} ou {{nome}}. *Não se aplica a AUTHENTICATION. |
headerText / footerText | string | não | Cabeçalho (texto) e rodapé. Para cabeçalho de mídia, use headerFormat abaixo. |
headerFormat | string | não | Tipo do cabeçalho: TEXT (padrão), IMAGE, VIDEO ou DOCUMENT. Para os três de mídia, informe também headerMediaUrl (ou headerHandle). |
headerMediaUrl | string | com header de mídia | URL pública https do arquivo de exemplo (imagem/vídeo/documento). Nós baixamos e enviamos à Meta para a aprovação. |
headerHandle | string | não | Alternativa avançada ao headerMediaUrl: um handle de mídia já enviado à Meta. Se informado, tem precedência. |
buttons | array | não | Até 10. Tipos abaixo. |
parameterFormat | string | não | POSITIONAL ({{1}}, padrão) ou NAMED ({{nome}}). Não misture os dois. |
messageSendTtlSeconds | integer | não | Validade da mensagem. Utility 30–43200 (12h); Marketing 43200–2592000 (30d); Auth 30–900 (15min). -1 = 30 dias (exceto Marketing). |
variableExamples | object | não | Exemplo por variável, pela chave como escrita: {"1": "João"} ou {"nome": "João"} |
codeExpirationMinutes | integer | não | Só Authentication. 1–90, padrão 10. |
addSecurityRecommendation | boolean | não | Só Authentication. Padrão true. |
otpButtonText | string | não | Só Authentication. Padrão "Copiar código". |
Tipos de botão
| type | Campos | Descrição |
|---|---|---|
QUICK_REPLY | text | Resposta rápida |
URL | text, url, urlExample | Link. 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_NUMBER | text, phoneNumber | Botão de ligar |
COPY_CODE | example | Copiar código/cupom (Marketing/Utility). Sem text — o WhatsApp rotula sozinho; example é um código de amostra. |
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.
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).
"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" }
]
}'
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
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
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
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
| Status | Significado |
|---|---|
sent | A Meta aceitou. Ainda não foi entregue. |
delivered | Entregue no aparelho do destinatário. |
read | Lida pelo destinatário. |
failed | Nã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
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
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
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.
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.
- Em Configurações → Webhooks, no cartão Chatwoot, informe a URL base do seu Chatwoot e salve.
- 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
Authentication Error.
- Crie uma caixa de entrada WhatsApp com provedor WhatsApp Cloud, usando o
phoneNumberIde owabaIdmostrados 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.
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étodo | Caminho | Descrição |
|---|---|---|
| POST | /{versão}/{phone_number_id}/messages | Texto, template, mídia e confirmação de leitura |
| POST | /{versão}/{phone_number_id}/media | Upload (multipart) |
| GET | /{versão}/{waba_id}/message_templates | Lista os templates |
| GET | /{versão}/{waba_id}/phone_numbers | Lista 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" }.
| Status | Significado | Quando acontece |
|---|---|---|
400 | Bad Request | Corpo inválido, campos obrigatórios ausentes, ou envio recusado (ex.: destinatário igual ao número remetente) |
401 | Unauthorized | Chave de API ausente, inválida ou inativa |
404 | Not Found | Recurso inexistente ou que não pertence à sua conta (ex.: mensagem por id) |
413 | Payload Too Large | Upload de mídia acima de 100 MB |
422 | Unprocessable Entity | Envio recusado pela regra da Meta — ex.: janela de 24h fechada |
429 | Too Many Requests | Limite 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-..."
}
(#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.