Chatbot para WhatsApp
API do WhatsApp
Integre sistemas, dispositivos e plataformas ao atendimento por WhatsApp: envie mensagens, consulte contatos e tickets e receba eventos em tempo real.
- URL base
https://wa.aembi.com/api/v1- Formato
JSON · UTF-8- Protocolo
somente HTTPS
A versão faz parte do caminho (/api/v1). Mudanças incompatíveis só acontecem numa nova versão; dentro da v1, apenas adicionamos campos e endpoints. Por isso, sua integração deve ignorar campos desconhecidos nas respostas.
Início rápido
- Solicite uma chave de API ao administrador da sua conta. Ele gera a chave no painel, no menu API, só com as permissões que a sua integração precisa.
- Guarde a chave numa variável de ambiente do seu servidor (nos exemplos,
WA_API_KEY). - Valide a integração chamando o endpoint abaixo. Ele deve retornar o nome da sua empresa.
curl -X GET "https://wa.aembi.com/api/v1/me" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/me", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/me');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Próximos passos
- Eventos em tempo real: cadastre um webhook para receber mensagens, status de entrega e encerramentos sem precisar consultar a API. Veja o guia de webhooks.
- Segurança: se o seu servidor tiver IP fixo, informe-o ao administrador para que a chave aceite chamadas só dele.
- Suporte: guarde o cabeçalho
X-Request-Iddas respostas nos seus logs. Com ele, o administrador encontra a chamada exata no painel.
Autenticação
Envie a chave em toda requisição, de preferência no cabeçalho Authorization:
Authorization: Bearer wak_0a1b2c3d4e5f_SEU_SEGREDO_DE_32_CARACTERESComo alternativa, também é aceito o cabeçalho X-API-Key: wak_....
As chaves começam com wak_, seguido de um prefixo público de 12 caracteres e do segredo. Guardamos apenas um hash, então a chave completa aparece uma única vez, na criação. Se ela for perdida, é preciso gerar outra.
Cada chave pode ter restrição de IP, data de validade e ser revogada a qualquer momento. Recomendamos uma chave por integração: se uma vazar, você revoga só aquela.
Isolamento por empresa
A plataforma é multiempresa. A chave identifica a sua empresa, então você nunca informa um ID de empresa nas requisições. Todos os dados retornados e todas as ações executadas ficam restritos à empresa dona da chave. Um recurso de outra empresa se comporta como inexistente e retorna 404 not_found.
A URL base pode ser qualquer domínio da plataforma: quem define a empresa é sempre a chave.
Escopos
Cada chave recebe só as permissões de que a integração precisa. Chamar um endpoint sem o escopo exigido retorna 403 insufficient_scope.
| Escopo | Permite |
|---|---|
* | Acesso total a todos os recursos da empresa. Use só em integrações internas. |
messages:send | Enviar mensagens (texto, mídia e documentos). |
messages:read | Consultar mensagens, conversas e status de entrega, e baixar mídias. |
contacts:read | Consultar contatos. |
contacts:write | Criar e atualizar contatos. |
tickets:read | Consultar tickets e atendimentos. |
tickets:write | Abrir tickets no ERP a partir de conversas. |
conversations:write | Transferir conversas entre departamentos e encerrá-las. |
webhooks:manage | Cadastrar e remover webhooks. |
Respostas e erros
As respostas de sucesso trazem o conteúdo em data:
{
"data": {
"...": "..."
}
}Os erros seguem sempre o mesmo formato, com um code estável e uma message descritiva. Para lógica de tratamento, use o campo code: a message é descritiva e pode mudar.
Toda resposta traz o cabeçalho X-Request-Id. Registre esse valor nos seus logs e informe-o ao suporte ao relatar um problema: com ele localizamos a requisição exata. O administrador da sua conta também consulta as chamadas dos últimos 30 dias no painel, no menu API.
| HTTP | Código | Quando ocorre |
|---|---|---|
| 401 | unauthorized | Chave ausente, em formato inválido, revogada ou expirada. |
| 403 | tenant_suspended | A empresa dona da chave está suspensa. |
| 403 | ip_not_allowed | A chave tem restrição de IP e a requisição veio de outro endereço. |
| 403 | insufficient_scope | A chave não tem o escopo exigido pelo endpoint. |
| 404 | not_found | Recurso inexistente ou pertencente a outra empresa. |
| 422 | validation_error | Corpo ou parâmetros inválidos. A mensagem indica o campo. |
| 409 | no_connection | Não há conexão de atendimento conectada para enviar. |
| 409 | connection_offline | A conexão informada está desconectada. |
| 409 | contact_opted_out | O contato pediu para não receber mensagens. Use transactional: true só para mensagens transacionais. |
| 409 | contact_exists | Já existe um contato com este número. A mensagem traz o id do existente. |
| 409 | conversation_closed | A conversa já está encerrada ou o encerramento já está em andamento. |
| 409 | ticket_in_progress | Já existe uma abertura de ticket em andamento para esta conversa. |
| 502 | erp_unavailable | O ERP recusou ou não respondeu à abertura do ticket. A mensagem traz o motivo. |
| 404 | media_not_found | A mensagem não tem mídia. |
| 410 | media_expired | O arquivo da mídia não está mais disponível no servidor. |
| 429 | rate_limited | Limite de requisições ou de envios por minuto excedido. Aguarde o tempo indicado em Retry-After. |
| 500 | internal_error | Erro inesperado. Informe o requestId ao suporte. |
Limites de uso
Para proteger a plataforma e os números de WhatsApp, cada chave tem um limite de requisições por minuto, e cada empresa tem um limite de envios de mensagem por minuto.
| Limite | Padrão | Aplica-se a |
|---|---|---|
| Requisições | 120 por minuto | Cada chave de API, em todos os endpoints. O administrador pode ampliar ao criar a chave. |
| Envios de mensagem | 60 por minuto | A empresa toda, somando todas as chaves, em POST /api/v1/messages. |
As janelas seguem o minuto do relógio. Toda resposta autenticada informa o consumo:
| Cabeçalho | Conteúdo |
|---|---|
X-RateLimit-Limit | Limite de requisições da chave por minuto. |
X-RateLimit-Remaining | Quantas ainda restam na janela atual. |
X-RateLimit-Reset | Segundos até a janela reiniciar. |
Ao exceder, a resposta é 429 rate_limited, com o cabeçalho Retry-After em segundos. Aguarde esse tempo antes de tentar de novo. Em disparos grandes, distribua os envios ao longo do tempo. Repetir a mesma requisição com a mesma Idempotency-Key é seguro e não conta como envio novo.
Conta
Verificar chave
GET/api/v1/me
Retorna a empresa (tenant) e as permissões da chave usada. Ideal para validar a integração na primeira configuração.
Escopo exigido: nenhum (qualquer chave válida)
Exemplo de requisição
curl -X GET "https://wa.aembi.com/api/v1/me" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/me", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/me');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"tenant": {
"id": "cm1a2b3c4d5e6f7g8h9i0j",
"slug": "sua-empresa",
"name": "Sua Empresa"
},
"key": {
"name": "Integração ERP",
"scopes": [
"messages:send",
"contacts:read"
],
"rateLimitPerMinute": 120
},
"ip": "203.0.113.10"
}
}Mensagens
Enviar mensagem
POST/api/v1/messages
Coloca a mensagem na fila de envio e responde na hora com status queued; acompanhe a entrega pelo endpoint de consulta. Envie o cabeçalho Idempotency-Key (até 100 caracteres, ex.: o número da fatura) para poder repetir a requisição com segurança: a mesma chave devolve a mesma mensagem, sem reenviar. Por padrão, o envio sai pela conexão de atendimento. Mídias podem ser enviadas por URL https ou em base64, com até 25 MB.
Escopo exigido: messages:send
Corpo da requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | Sim | Número com DDI e DDD, só dígitos. Números brasileiros sem 55 são completados. Ex.: 5518999999999 |
type | string | Não | text (padrão), image, video, audio ou document. |
text | string | Não | Texto da mensagem, obrigatório para text. Até 4096 caracteres, com formatação do WhatsApp (*negrito*, _itálico_). |
mediaUrl | string | Não | URL https da mídia. Informe mediaUrl ou mediaBase64. |
mediaBase64 | string | Não | Conteúdo da mídia em base64, com ou sem o prefixo data:. |
mimeType | string | Não | Tipo do arquivo, ex.: application/pdf. Obrigatório com mediaBase64. |
fileName | string | Não | Nome exibido para documentos, ex.: fatura-2026-09.pdf. |
caption | string | Não | Legenda de imagem, vídeo ou documento (até 1024 caracteres). |
connection | string | Não | Nome da conexão de envio. Padrão: a conexão de atendimento. |
name | string | Não | Nome do contato, usado quando ele ainda não existe. |
transactional | boolean | Não | true para mensagens transacionais (fatura, aviso técnico), que são entregues mesmo a contatos descadastrados de campanhas. Padrão: false. |
Exemplo de requisição
curl -X POST "https://wa.aembi.com/api/v1/messages" \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Idempotency-Key: fatura-2026-09-10452" \
-H "Content-Type: application/json" \
-d '{
"to": "5518999999999",
"type": "document",
"mediaUrl": "https://exemplo.com.br/faturas/2026-09.pdf",
"fileName": "fatura-2026-09.pdf",
"caption": "Sua fatura de setembro",
"transactional": true
}'const res = await fetch("https://wa.aembi.com/api/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
"Idempotency-Key": "fatura-2026-09-10452",
"Content-Type": "application/json",
},
body: JSON.stringify({
"to": "5518999999999",
"type": "document",
"mediaUrl": "https://exemplo.com.br/faturas/2026-09.pdf",
"fileName": "fatura-2026-09.pdf",
"caption": "Sua fatura de setembro",
"transactional": true
}),
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/messages');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
'Idempotency-Key: fatura-2026-09-10452',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'to' => '5518999999999',
'type' => 'document',
'mediaUrl' => 'https://exemplo.com.br/faturas/2026-09.pdf',
'fileName' => 'fatura-2026-09.pdf',
'caption' => 'Sua fatura de setembro',
'transactional' => true,
], JSON_UNESCAPED_UNICODE),
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.post(
"https://wa.aembi.com/api/v1/messages",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}", "Idempotency-Key": "fatura-2026-09-10452"},
json={
"to": "5518999999999",
"type": "document",
"mediaUrl": "https://exemplo.com.br/faturas/2026-09.pdf",
"fileName": "fatura-2026-09.pdf",
"caption": "Sua fatura de setembro",
"transactional": True,
},
timeout=30,
)
print(res.status_code, res.json())Resposta 202
{
"data": {
"id": "cmv1a2b3c4d5e6f7g8h9i0j1",
"status": "queued",
"type": "document",
"to": "5518999999999",
"connection": "principal",
"conversationId": null,
"protocol": null,
"error": null,
"createdAt": "2026-09-29T13:20:00.000Z",
"sentAt": null
}
}Consultar mensagem
GET/api/v1/messages/{id}
Retorna o status atual de uma mensagem enviada pela API: queued (na fila), sent (enviada), delivered (entregue), read (lida) ou failed (falhou; o motivo vem em error).
Escopo exigido: messages:read
Parâmetros de caminho
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | ID retornado no envio. |
Exemplo de requisição
curl -X GET "https://wa.aembi.com/api/v1/messages/SEU_ID" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/messages/SEU_ID", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/messages/SEU_ID');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/messages/SEU_ID",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"id": "cmv1a2b3c4d5e6f7g8h9i0j1",
"status": "delivered",
"type": "document",
"to": "5518999999999",
"connection": "principal",
"conversationId": "cmv1k2l3m4n5o6p7q8r9s0t1",
"protocol": "20260929-000123",
"error": null,
"createdAt": "2026-09-29T13:20:00.000Z",
"sentAt": "2026-09-29T13:20:03.000Z"
}
}Baixar mídia
GET/api/v1/messages/{id}/media
Retorna o arquivo de uma mensagem com mídia (imagem, áudio, vídeo ou documento), recebida ou enviada. Aceita o id que vem no webhook message.received (o link pronto está em mediaUrl) ou o id retornado no envio pela API. Os erros continuam em JSON.
Escopo exigido: messages:read
Parâmetros de caminho
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | ID da mensagem (webhook) ou ID do envio pela API. |
Exemplo de requisição
# -O -J salva com o nome original do arquivo
curl -fOJ "https://wa.aembi.com/api/v1/messages/SEU_ID/media" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/messages/SEU_ID/media", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
if (!res.ok) throw new Error((await res.json()).message);
const nome = res.headers.get("content-disposition")?.match(/filename="?([^";]+)/)?.[1] ?? "arquivo";
await (await import("node:fs/promises")).writeFile(nome, Buffer.from(await res.arrayBuffer()));$ch = curl_init('https://wa.aembi.com/api/v1/messages/SEU_ID/media');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status === 200) file_put_contents('arquivo', $resposta);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/messages/SEU_ID/media",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
res.raise_for_status()
open("arquivo", "wb").write(res.content)Resposta 200
O próprio arquivo, com os cabeçalhos Content-Type (tipo do arquivo), Content-Length e Content-Disposition (nome original).
Contatos
Listar contatos
GET/api/v1/contacts
Lista os contatos da empresa, dos mais recentes para os mais antigos. Quando houver mais resultados, nextCursor vem preenchido: envie esse valor em cursor para buscar a próxima página.
Escopo exigido: contacts:read
Parâmetros de consulta
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
q | string | Não | Busca por nome ou por parte do número. |
waId | string | Não | Número exato, ex.: 5518999999999. |
tag | string | Não | Somente contatos com esta tag. |
erpClienteId | string | Não | Somente contatos vinculados a este cliente do ERP. |
optOut | boolean | Não | true para descadastrados de campanhas; false para os demais. |
limit | integer | Não | Itens por página, de 1 a 100. Padrão: 50. |
cursor | string | Não | Valor de nextCursor da página anterior. |
Exemplo de requisição
curl -X GET "https://wa.aembi.com/api/v1/contacts" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/contacts", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/contacts');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/contacts",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"items": [
{
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza",
"pushName": "Maria",
"tags": [
"cliente",
"fibra"
],
"notes": null,
"erpClienteId": "10452",
"erpContatoId": null,
"birthday": "1990-05-14",
"optOut": false,
"optOutAt": null,
"verified": true,
"createdAt": "2026-08-02T14:10:00.000Z",
"updatedAt": "2026-09-29T16:00:00.000Z"
}
],
"nextCursor": "cmu9z8y7x6w5v4u3t2s1r0q9"
}
}Consultar contato
GET/api/v1/contacts/{id}
Retorna o contato e as suas 5 conversas mais recentes.
Escopo exigido: contacts:read
Parâmetros de caminho
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | ID do contato ou o número, ex.: 5518999999999. |
Exemplo de requisição
curl -X GET "https://wa.aembi.com/api/v1/contacts/SEU_ID" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/contacts/SEU_ID", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/contacts/SEU_ID');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/contacts/SEU_ID",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza",
"pushName": "Maria",
"tags": [
"cliente",
"fibra"
],
"notes": null,
"erpClienteId": "10452",
"erpContatoId": null,
"birthday": "1990-05-14",
"optOut": false,
"optOutAt": null,
"verified": true,
"createdAt": "2026-08-02T14:10:00.000Z",
"updatedAt": "2026-09-29T16:00:00.000Z",
"recentConversations": [
{
"id": "cmv1k2l3m4n5o6p7q8r9s0t1",
"protocol": "202609290012",
"status": "closed",
"contact": {
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza"
},
"connection": "principal",
"department": "Financeiro",
"agent": "João Lima",
"tickets": [
{
"id": "8812",
"number": "61234"
}
],
"rating": {
"score": 5,
"comment": "Atendimento rápido"
},
"openedAt": "2026-09-29T15:40:00.000Z",
"lastMessageAt": "2026-09-29T16:09:00.000Z",
"closedAt": "2026-09-29T16:10:00.000Z"
}
]
}
}Cadastrar contato
POST/api/v1/contacts
Cadastra um contato. Se o número já existir, retorna 409 contact_exists com o id do contato existente; nesse caso, use o PATCH.
Escopo exigido: contacts:write
Corpo da requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
waId | string | Sim | Número com DDI e DDD. Números brasileiros sem 55 são completados. |
name | string | Não | Nome do contato (até 120 caracteres). |
tags | string[] | Não | Lista de tags (até 30, com até 40 caracteres cada). |
notes | string | Não | Observações internas (até 2000 caracteres). |
erpClienteId | string | Não | ID do cliente no ERP. |
erpContatoId | string | Não | ID do contato no ERP. |
birthday | string | Não | Data de nascimento no formato AAAA-MM-DD. |
optOut | boolean | Não | true se o contato não quer receber campanhas. |
Exemplo de requisição
curl -X POST "https://wa.aembi.com/api/v1/contacts" \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"waId": "5518999999999",
"name": "Maria Souza",
"tags": [
"cliente",
"fibra"
],
"erpClienteId": "10452"
}'const res = await fetch("https://wa.aembi.com/api/v1/contacts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"waId": "5518999999999",
"name": "Maria Souza",
"tags": [
"cliente",
"fibra"
],
"erpClienteId": "10452"
}),
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/contacts');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'waId' => '5518999999999',
'name' => 'Maria Souza',
'tags' => [
'cliente',
'fibra',
],
'erpClienteId' => '10452',
], JSON_UNESCAPED_UNICODE),
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.post(
"https://wa.aembi.com/api/v1/contacts",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
json={
"waId": "5518999999999",
"name": "Maria Souza",
"tags": [
"cliente",
"fibra",
],
"erpClienteId": "10452",
},
timeout=30,
)
print(res.status_code, res.json())Resposta 201
{
"data": {
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza",
"pushName": "Maria",
"tags": [
"cliente",
"fibra"
],
"notes": null,
"erpClienteId": "10452",
"erpContatoId": null,
"birthday": "1990-05-14",
"optOut": false,
"optOutAt": null,
"verified": false,
"createdAt": "2026-08-02T14:10:00.000Z",
"updatedAt": "2026-09-29T16:00:00.000Z"
}
}Atualizar contato
PATCH/api/v1/contacts/{id}
Altera somente os campos enviados; null limpa o campo. Use addTags e removeTags para mexer nas tags sem reenviar a lista. Ao trocar o erpClienteId, a verificação de identidade é desfeita (verified volta a false): o cliente precisa confirmar a identidade de novo no WhatsApp antes de receber faturas.
Escopo exigido: contacts:write
Parâmetros de caminho
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | ID do contato ou o número. |
Corpo da requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string | Não | Nome do contato (até 120 caracteres). |
tags | string[] | Não | Lista de tags (até 30, com até 40 caracteres cada). |
notes | string | Não | Observações internas (até 2000 caracteres). |
erpClienteId | string | Não | ID do cliente no ERP. |
erpContatoId | string | Não | ID do contato no ERP. |
birthday | string | Não | Data de nascimento no formato AAAA-MM-DD. |
optOut | boolean | Não | true se o contato não quer receber campanhas. |
addTags | string[] | Não | Tags a acrescentar. |
removeTags | string[] | Não | Tags a remover. |
Exemplo de requisição
curl -X PATCH "https://wa.aembi.com/api/v1/contacts/SEU_ID" \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"addTags": [
"inadimplente"
],
"notes": "Prefere contato à tarde"
}'const res = await fetch("https://wa.aembi.com/api/v1/contacts/SEU_ID", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"addTags": [
"inadimplente"
],
"notes": "Prefere contato à tarde"
}),
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/contacts/SEU_ID');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'addTags' => [
'inadimplente',
],
'notes' => 'Prefere contato à tarde',
], JSON_UNESCAPED_UNICODE),
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.patch(
"https://wa.aembi.com/api/v1/contacts/SEU_ID",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
json={
"addTags": [
"inadimplente",
],
"notes": "Prefere contato à tarde",
},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza",
"pushName": "Maria",
"tags": [
"cliente",
"fibra",
"inadimplente"
],
"notes": "Prefere contato à tarde",
"erpClienteId": "10452",
"erpContatoId": null,
"birthday": "1990-05-14",
"optOut": false,
"optOutAt": null,
"verified": true,
"createdAt": "2026-08-02T14:10:00.000Z",
"updatedAt": "2026-09-29T16:00:00.000Z"
}
}Conversas
Listar conversas
GET/api/v1/conversations
Lista as conversas da empresa, da última mensagem mais recente para a mais antiga, com filtros e paginação por cursor.
Escopo exigido: messages:read
Parâmetros de consulta
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
contactId | string | Não | Somente conversas deste contato. |
waId | string | Não | Somente conversas deste número. |
status | string | Não | bot, queued (na fila), open (com atendente) ou closed. |
since | string | Não | Somente conversas com mensagens a partir desta data, ex.: 2026-09-01. |
limit | integer | Não | Itens por página, de 1 a 100. Padrão: 50. |
cursor | string | Não | Valor de nextCursor da página anterior. |
Exemplo de requisição
curl -X GET "https://wa.aembi.com/api/v1/conversations" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/conversations", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/conversations');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/conversations",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"items": [
{
"id": "cmv1k2l3m4n5o6p7q8r9s0t1",
"protocol": "202609290012",
"status": "closed",
"contact": {
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza"
},
"connection": "principal",
"department": "Financeiro",
"agent": "João Lima",
"tickets": [
{
"id": "8812",
"number": "61234"
}
],
"rating": {
"score": 5,
"comment": "Atendimento rápido"
},
"openedAt": "2026-09-29T15:40:00.000Z",
"lastMessageAt": "2026-09-29T16:09:00.000Z",
"closedAt": "2026-09-29T16:10:00.000Z"
}
],
"nextCursor": null
}
}Consultar conversa
GET/api/v1/conversations/{id}
Retorna a conversa com departamento, atendente, tickets, avaliação e as últimas mensagens em ordem cronológica. Mensagens com mídia trazem mediaUrl para download.
Escopo exigido: messages:read
Parâmetros de caminho
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | ID da conversa ou o protocolo, ex.: 202609290012. |
Parâmetros de consulta
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
messages | integer | Não | Quantas das últimas mensagens retornar, de 0 a 200. Padrão: 50. |
Exemplo de requisição
curl -X GET "https://wa.aembi.com/api/v1/conversations/SEU_ID" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/conversations/SEU_ID", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/conversations/SEU_ID');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/conversations/SEU_ID",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"id": "cmv1k2l3m4n5o6p7q8r9s0t1",
"protocol": "202609290012",
"status": "closed",
"contact": {
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza"
},
"connection": "principal",
"department": "Financeiro",
"agent": "João Lima",
"tickets": [
{
"id": "8812",
"number": "61234"
}
],
"rating": {
"score": 5,
"comment": "Atendimento rápido"
},
"openedAt": "2026-09-29T15:40:00.000Z",
"lastMessageAt": "2026-09-29T16:09:00.000Z",
"closedAt": "2026-09-29T16:10:00.000Z",
"messages": [
{
"id": "cmw1a2b3c4d5e6f7g8h9i0j1",
"direction": "in",
"senderType": "contact",
"agent": null,
"type": "text",
"text": "Oi, preciso da segunda via da fatura",
"fileName": null,
"mimeType": null,
"mediaUrl": null,
"status": null,
"createdAt": "2026-09-29T15:40:00.000Z"
}
],
"hasMoreMessages": false
}
}Listar departamentos
GET/api/v1/departments
Lista os departamentos ativos da empresa, usados na transferência de conversas e na abertura de tickets.
Escopo exigido: nenhum (qualquer chave válida)
Exemplo de requisição
curl -X GET "https://wa.aembi.com/api/v1/departments" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/departments", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/departments');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/departments",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": [
{
"id": "cmd1a2b3c4d5e6f7g8h9i0j1",
"name": "Financeiro",
"slug": "financeiro"
},
{
"id": "cmd2a2b3c4d5e6f7g8h9i0j2",
"name": "Suporte",
"slug": "suporte"
}
]
}Transferir conversa
POST/api/v1/conversations/{id}/transfer
Transfere a conversa para outro departamento e a coloca na fila, como no painel. O cliente recebe o aviso "Estou transferindo você para <departamento>..." e, se a conversa tiver ticket, a transferência é registrada nele como comentário interno.
Escopo exigido: conversations:write
Parâmetros de caminho
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | ID ou protocolo da conversa. |
Corpo da requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
department | string | Sim | Nome, slug ou id do departamento de destino. |
agent | string | Não | E-mail de um atendente, para direcionar a conversa a ele. |
note | string | Não | Observação registrada no ticket (até 1000 caracteres). |
Exemplo de requisição
curl -X POST "https://wa.aembi.com/api/v1/conversations/SEU_ID/transfer" \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"department": "Financeiro",
"note": "Cliente pediu renegociação"
}'const res = await fetch("https://wa.aembi.com/api/v1/conversations/SEU_ID/transfer", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"department": "Financeiro",
"note": "Cliente pediu renegociação"
}),
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/conversations/SEU_ID/transfer');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'department' => 'Financeiro',
'note' => 'Cliente pediu renegociação',
], JSON_UNESCAPED_UNICODE),
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.post(
"https://wa.aembi.com/api/v1/conversations/SEU_ID/transfer",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
json={
"department": "Financeiro",
"note": "Cliente pediu renegociação",
},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"id": "cmv1k2l3m4n5o6p7q8r9s0t1",
"protocol": "202609290012",
"status": "queued",
"contact": {
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza"
},
"connection": "principal",
"department": "Financeiro",
"agent": null,
"tickets": [
{
"id": "8812",
"number": "61234"
}
],
"rating": {
"score": 5,
"comment": "Atendimento rápido"
},
"openedAt": "2026-09-29T15:40:00.000Z",
"lastMessageAt": "2026-09-29T16:09:00.000Z",
"closedAt": null
}
}Encerrar conversa
POST/api/v1/conversations/{id}/close
Encerra a conversa pelo mesmo fluxo do painel, com o histórico enviado ao ticket. Se ela passou por atendimento humano ou tem ticket, e ainda não foi avaliada, o cliente recebe a pesquisa de satisfação e a conversa fecha após a resposta (ou em 2 horas); nos demais casos, fecha na hora com a mensagem de encerramento. Envie survey: false para fechar na hora, sem pesquisa. A resposta é 202; a conclusão chega pelo webhook conversation.closed.
Escopo exigido: conversations:write
Parâmetros de caminho
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | ID ou protocolo da conversa. |
Corpo da requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
survey | boolean | Não | false para encerrar sem a pesquisa de satisfação. Padrão: true. |
Exemplo de requisição
curl -X POST "https://wa.aembi.com/api/v1/conversations/SEU_ID/close" \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"survey": false
}'const res = await fetch("https://wa.aembi.com/api/v1/conversations/SEU_ID/close", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"survey": false
}),
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/conversations/SEU_ID/close');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'survey' => false,
], JSON_UNESCAPED_UNICODE),
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.post(
"https://wa.aembi.com/api/v1/conversations/SEU_ID/close",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
json={
"survey": False,
},
timeout=30,
)
print(res.status_code, res.json())Resposta 202
{
"data": {
"id": "cmv1k2l3m4n5o6p7q8r9s0t1",
"protocol": "202609290012",
"status": "closing",
"survey": false
}
}Tickets
Listar tickets
GET/api/v1/tickets
Lista os tickets abertos no ERP a partir de conversas do WhatsApp, do mais recente para o mais antigo.
Escopo exigido: tickets:read
Parâmetros de consulta
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
contactId | string | Não | Somente tickets deste contato. |
waId | string | Não | Somente tickets deste número. |
conversationId | string | Não | ID ou protocolo da conversa. |
number | string | Não | Número do chamado no ERP. |
since | string | Não | Somente tickets abertos a partir desta data, ex.: 2026-09-01. |
limit | integer | Não | Itens por página, de 1 a 100. Padrão: 50. |
cursor | string | Não | Valor de nextCursor da página anterior. |
Exemplo de requisição
curl -X GET "https://wa.aembi.com/api/v1/tickets" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/tickets", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/tickets');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/tickets",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"items": [
{
"id": "cmt1a2b3c4d5e6f7g8h9i0j1",
"ticketId": "8812",
"number": "61234",
"conversationId": "cmv1k2l3m4n5o6p7q8r9s0t1",
"protocol": "202609290012",
"contact": {
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza"
},
"createdAt": "2026-09-29T15:55:00.000Z"
}
],
"nextCursor": null
}
}Abrir ticket
POST/api/v1/tickets
Abre um ticket no ERP para uma conversa, pelo mesmo fluxo do bot: o histórico da conversa entra como comentário interno e as mídias como anexos. Cada conversa tem no máximo um ticket; se já existir, ele é retornado com created: false. Se o ERP demorar mais de 20 segundos, a resposta é 202 com status processing; consulte depois em GET /api/v1/tickets?conversationId=... ou aguarde o webhook ticket.created.
Escopo exigido: tickets:write
Corpo da requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
conversationId | string | Sim | ID ou protocolo da conversa. |
department | string | Não | Nome do departamento, ex.: Suporte. Padrão: o departamento da conversa. |
description | string | Não | Descrição do problema (até 4000 caracteres). |
Exemplo de requisição
curl -X POST "https://wa.aembi.com/api/v1/tickets" \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"conversationId": "202609290012",
"department": "Suporte",
"description": "Cliente relata lentidão desde ontem à noite"
}'const res = await fetch("https://wa.aembi.com/api/v1/tickets", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"conversationId": "202609290012",
"department": "Suporte",
"description": "Cliente relata lentidão desde ontem à noite"
}),
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/tickets');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'conversationId' => '202609290012',
'department' => 'Suporte',
'description' => 'Cliente relata lentidão desde ontem à noite',
], JSON_UNESCAPED_UNICODE),
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.post(
"https://wa.aembi.com/api/v1/tickets",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
json={
"conversationId": "202609290012",
"department": "Suporte",
"description": "Cliente relata lentidão desde ontem à noite",
},
timeout=30,
)
print(res.status_code, res.json())Resposta 201
{
"data": {
"id": "cmt1a2b3c4d5e6f7g8h9i0j1",
"ticketId": "8812",
"number": "61234",
"conversationId": "cmv1k2l3m4n5o6p7q8r9s0t1",
"protocol": "202609290012",
"contact": {
"id": "cmu1a2b3c4d5e6f7g8h9i0j1",
"waId": "5518999999999",
"name": "Maria Souza"
},
"createdAt": "2026-09-29T15:55:00.000Z",
"created": true
}
}Webhooks
Listar webhooks
GET/api/v1/webhooks
Lista os webhooks cadastrados para a sua empresa. O segredo não é retornado.
Escopo exigido: webhooks:manage
Exemplo de requisição
curl -X GET "https://wa.aembi.com/api/v1/webhooks" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/webhooks", {
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/webhooks');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.get(
"https://wa.aembi.com/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": [
{
"id": "cmx1a2b3c4d5e6f7g8h9i0j1",
"name": "ERP",
"url": "https://erp.exemplo.com.br/webhooks/wa",
"events": [
"message.received",
"ticket.created"
],
"active": true,
"failCount": 0,
"disabledReason": null,
"createdBy": "Maria Souza",
"createdAt": "2026-09-29T16:00:00.000Z"
}
]
}Cadastrar webhook
POST/api/v1/webhooks
Cadastra uma URL https para receber eventos. O secret retornado serve para verificar a assinatura e aparece somente nesta resposta. Limite de 10 webhooks por empresa.
Escopo exigido: webhooks:manage
Corpo da requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string | Sim | Nome de identificação, ex.: ERP. |
url | string | Sim | URL https que vai receber os eventos. |
events | string[] | Sim | Eventos desejados. Veja a lista no guia de webhooks. |
Exemplo de requisição
curl -X POST "https://wa.aembi.com/api/v1/webhooks" \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "ERP",
"url": "https://erp.exemplo.com.br/webhooks/wa",
"events": [
"message.received",
"message.status"
]
}'const res = await fetch("https://wa.aembi.com/api/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "ERP",
"url": "https://erp.exemplo.com.br/webhooks/wa",
"events": [
"message.received",
"message.status"
]
}),
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/webhooks');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'ERP',
'url' => 'https://erp.exemplo.com.br/webhooks/wa',
'events' => [
'message.received',
'message.status',
],
], JSON_UNESCAPED_UNICODE),
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.post(
"https://wa.aembi.com/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
json={
"name": "ERP",
"url": "https://erp.exemplo.com.br/webhooks/wa",
"events": [
"message.received",
"message.status",
],
},
timeout=30,
)
print(res.status_code, res.json())Resposta 201
{
"data": {
"id": "cmx1a2b3c4d5e6f7g8h9i0j1",
"name": "ERP",
"url": "https://erp.exemplo.com.br/webhooks/wa",
"events": [
"message.received",
"message.status"
],
"active": true,
"failCount": 0,
"disabledReason": null,
"createdBy": "API: Integração ERP",
"createdAt": "2026-09-29T16:00:00.000Z",
"secret": "whsec_6y0Qe9cR2vN4mT8bW1kZ3pL5sA7dF0gH"
}
}Excluir webhook
DELETE/api/v1/webhooks/{id}
Remove o webhook e o seu histórico de entregas.
Escopo exigido: webhooks:manage
Parâmetros de caminho
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string | Sim | ID do webhook. |
Exemplo de requisição
curl -X DELETE "https://wa.aembi.com/api/v1/webhooks/SEU_ID" \
-H "Authorization: Bearer $WA_API_KEY"const res = await fetch("https://wa.aembi.com/api/v1/webhooks/SEU_ID", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.WA_API_KEY}`,
},
});
const json = await res.json();
console.log(res.status, json);$ch = curl_init('https://wa.aembi.com/api/v1/webhooks/SEU_ID');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WA_API_KEY'),
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($resposta, true);import os
import requests
res = requests.delete(
"https://wa.aembi.com/api/v1/webhooks/SEU_ID",
headers={"Authorization": f"Bearer {os.environ['WA_API_KEY']}"},
timeout=30,
)
print(res.status_code, res.json())Resposta 200
{
"data": {
"id": "cmx1a2b3c4d5e6f7g8h9i0j1",
"deleted": true
}
}Guia de webhooks
Webhooks avisam o seu sistema em tempo real quando algo acontece, sem precisar consultar a API. Cadastre a URL no painel (menu API, seção Webhooks) ou pelo endpoint POST /api/v1/webhooks, escolhendo os eventos.
Eventos
| Evento | Quando ocorre |
|---|---|
message.received | Mensagem recebida de um cliente. |
message.status | Mudança de status de uma mensagem enviada (pela API, pelo painel ou pelo bot): sent, delivered, read ou failed. |
conversation.closed | Conversa encerrada pelo atendente, pelo bot ou por inatividade. |
conversation.rated | Cliente respondeu à pesquisa de satisfação. |
ticket.created | Ticket aberto no ERP a partir de uma conversa. |
O botão Testar do painel envia o evento ping, útil para validar a configuração.
Formato
Cada evento é um POST com corpo JSON: id, event, createdAt e o conteúdo em data, que depende do evento. Exemplo de message.received:
{
"id": "evt_9b1f2c7d4e6a8b0c1d2e3f4a5b6c7d8e",
"event": "message.received",
"createdAt": "2026-09-29T16:00:00.000Z",
"data": {
"id": "cmw1a2b3c4d5e6f7g8h9i0j1",
"conversationId": "cmv1k2l3m4n5o6p7q8r9s0t1",
"protocol": "202609290012",
"from": "5518999999999",
"contactName": "Maria Souza",
"type": "text",
"text": "Oi, preciso da segunda via da fatura",
"fileName": null,
"mimeType": null,
"mediaUrl": null,
"connection": "principal",
"receivedAt": "2026-09-29T16:00:00.000Z"
}
}Cabeçalhos
| Cabeçalho | Conteúdo |
|---|---|
X-WA-Event | Nome do evento, ex.: message.received. |
X-WA-Event-Id | ID único do evento. É o mesmo nas novas tentativas e nos reenvios. |
X-WA-Delivery | ID desta entrega específica, para suporte. |
X-WA-Timestamp | Momento do envio, em segundos (Unix). |
X-WA-Signature | Assinatura sha256=... do corpo. |
Verificando a assinatura
Calcule o HMAC-SHA256 de timestamp + "." + corpo com o segredo do webhook (whsec_...) e compare com X-WA-Signature. Use o corpo bruto, exatamente como recebido, e rejeite timestamps com mais de 5 minutos, para que ninguém consiga reaproveitar um evento capturado.
import crypto from "node:crypto";
import express from "express";
const app = express();
// use o corpo bruto: a assinatura é calculada sobre os bytes exatos recebidos
app.post("/webhooks/wa", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("X-WA-Timestamp") || "";
const recebida = req.get("X-WA-Signature") || "";
const esperada = "sha256=" + crypto
.createHmac("sha256", process.env.WA_WEBHOOK_SECRET)
.update(ts + "." + req.body)
.digest("hex");
const valida = recebida.length === esperada.length &&
crypto.timingSafeEqual(Buffer.from(recebida), Buffer.from(esperada));
if (!valida || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const evento = JSON.parse(req.body);
res.sendStatus(200); // responda rápido
processar(evento); // e processe em segundo plano, ignorando evento.id repetido
});<?php
// corpo bruto, exatamente como recebido
$corpo = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_WA_TIMESTAMP'] ?? '';
$recebida = $_SERVER['HTTP_X_WA_SIGNATURE'] ?? '';
$esperada = 'sha256=' . hash_hmac('sha256', $ts . '.' . $corpo, getenv('WA_WEBHOOK_SECRET'));
if (!hash_equals($esperada, $recebida) || abs(time() - (int) $ts) > 300) {
http_response_code(401);
exit;
}
$evento = json_decode($corpo, true);
http_response_code(200); // responda rápido
// processe em segundo plano (fila), ignorando $evento['id'] repetidoimport hashlib
import hmac
import json
import os
import time
from flask import Flask, request
app = Flask(__name__)
@app.post("/webhooks/wa")
def webhook():
corpo = request.get_data() # bytes brutos, exatamente como recebidos
ts = request.headers.get("X-WA-Timestamp", "")
recebida = request.headers.get("X-WA-Signature", "")
esperada = "sha256=" + hmac.new(
os.environ["WA_WEBHOOK_SECRET"].encode(),
ts.encode() + b"." + corpo,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(esperada, recebida) or abs(time.time() - int(ts or 0)) > 300:
return "", 401
evento = json.loads(corpo)
# responda rápido e processe em segundo plano, ignorando evento["id"] repetido
return "", 200Entrega e novas tentativas
- Responda com qualquer status 2xx em até 10 segundos. Se o processamento for demorado, faça em segundo plano.
- Sem resposta 2xx, tentamos de novo após 1 min, 5 min, 30 min, 2 h e 6 h. Redirecionamentos não são seguidos.
- A entrega é pelo menos uma vez: um evento pode chegar repetido ou fora de ordem. Use o
iddo evento para ignorar repetições. - Depois de 10 eventos seguidos sem sucesso, o webhook é desativado automaticamente. Ele pode ser reativado pelo painel.
- Em
message.status, mensagens enviadas pela API trazemapiMessageId, o mesmoidretornado no envio.
Histórico de versões
29/09/2026
- Lançamento da API v1: autenticação por chave, escopos, restrição por IP, endpoint /me, documentação e especificação OpenAPI.
- Envio de mensagens (texto, imagem, vídeo, áudio e documento) com fila, idempotência e consulta de status.
- Gestão de chaves pelo painel, no menu API: permissões, IPs, validade e revogação.
- Webhooks de eventos assinados com HMAC-SHA256, com novas tentativas e histórico de entregas.
- Limites de uso: requisições por chave e envios por empresa, com cabeçalhos X-RateLimit e erro 429.
- Download de mídia (GET /messages/{id}/media) e campo mediaUrl no webhook message.received.
- Contatos (listar, consultar, cadastrar e atualizar) e conversas (listar e consultar com o histórico).
- Tickets: listagem e abertura no ERP pelo mesmo fluxo do bot, com histórico e anexos.
- Conversas: transferência entre departamentos, encerramento (com ou sem pesquisa) e lista de departamentos.
- Registro de chamadas: todas as requisições ficam consultáveis por 30 dias no painel, com busca por requestId.
X-Request-Id da chamada.