Eventos do webhook
A cada evento, a Atalia faz um POST com JSON para o endereço cadastrado em Chave e webhook. Esta página traz o formato exato de cada um, com exemplos completos para você (ou a sua IA) escrever o código que recebe.
Como chega
| Header | Exemplo | O que é |
|---|---|---|
Content-Type | application/json | Sempre JSON em UTF-8. |
X-Atalia-Event | message.received | O tipo do evento, o mesmo do campo type. |
X-Atalia-Event-Id | evt_9f2c4b1a7e3d5c6b8a0f1e2d | O id do evento, o mesmo do campo id. |
X-Atalia-Signature | t=1793206931,v1=5c8f… | A assinatura do corpo. Veja Verificar a assinatura. |
User-Agent | Atalia-Webhooks/1.0 |
Todo evento tem o mesmo envelope:
{
"id": "evt_9f2c4b1a7e3d5c6b8a0f1e2d",
"type": "message.received",
"created_at": "2026-10-28T17:02:11.482Z",
"company_id": "6b1f2c9e-4d3a-4c55-9a51-3f7c1e2d9b10",
"client_id": null,
"channel": { "id": "ch_2f81a9c0d1", "type": "whatsapp", "phone": "+55 11 98765-4321" },
"data": { }
}
| Campo | O que é |
|---|---|
id | Id único do evento (evt_ + 24 caracteres). Use para ignorar repetidos. |
type | O tipo do evento (lista abaixo). |
created_at | Quando o evento foi criado, em ISO 8601 (UTC). |
company_id | A sua conta na Atalia. |
client_id | Só para revenda: o cliente dono do canal. null nos seus próprios canais. |
channel | A caixa de entrada: id (ch_...), type (whatsapp, whatsapp_web, instagram ou messenger) e o identificador dela (phone no WhatsApp, username no Instagram, page_name no Messenger). null nos eventos que não são de um canal. |
data | O conteúdo, diferente em cada tipo. |
O created_at do envelope é ISO 8601 em UTC. O created_at de dentro da mensagem (data.message.created_at) é o horário em que ela foi gravada, no formato "YYYY-MM-DD HH:MM:SS" e no horário de Brasília, igual ao de GET /api/v1/messages/{id}.
Os eventos
| Evento | Quando |
|---|---|
message.received | Um cliente mandou mensagem. |
message.echo | Alguém respondeu pelo app (WhatsApp Business em coexistência, Instagram ou Messenger). |
message.status | Uma mensagem enviada mudou de situação. |
channel.connected | Uma caixa de entrada foi conectada ou reconectada. |
channel.disconnected | Uma caixa de entrada foi apagada. |
client.connected | Revenda: um cliente conectou o primeiro canal pela sua página de conexão. |
client.no_slot | Revenda: um cliente tentou conectar e o seu pacote estava cheio. |
ping | Teste enviado pelo botão "Enviar mensagem de teste" ou por POST /api/v1/webhook/test. |
message.received
Um cliente mandou mensagem. Para responder, use o conversation_id em POST /api/v1/messages.
{
"id": "evt_9f2c4b1a7e3d5c6b8a0f1e2d",
"type": "message.received",
"created_at": "2026-10-28T17:02:11.482Z",
"company_id": "6b1f2c9e-4d3a-4c55-9a51-3f7c1e2d9b10",
"client_id": null,
"channel": { "id": "ch_2f81a9c0d1", "type": "whatsapp", "phone": "+55 11 98765-4321" },
"data": {
"conversation_id": "48213",
"message": {
"id": "6f1c2a7e-0b8e-4d0a-9a51-3f7c1e2d9b10",
"external_id": "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABIYFjNFQjA",
"direction": "inbound",
"type": "text",
"content_type": null,
"text": "Oi! Vocês entregam no centro hoje?",
"media": null,
"status": "delivered",
"contact": { "name": "Maria Souza", "phone": "+5511988887777" },
"created_at": "2026-10-28 14:02:11"
}
}
}
Campo de data.message | O que é |
|---|---|
id | Id da mensagem na Atalia. |
external_id | Id da mensagem no WhatsApp, Instagram ou Messenger. |
direction | inbound (do cliente) ou outbound (da empresa). |
type | text, image, video, audio, document, sticker, interactive, template e outros. |
content_type | Detalhe do tipo, quando há. Ex.: interactive_button_reply quando o cliente tocou num botão (o título do botão vem em text). |
text | O texto da mensagem, a legenda da mídia ou o título do botão tocado. null em template. |
template | Só quando type é template: o template enviado. |
media | null ou { mime_type, download_path }. Veja o exemplo abaixo. |
status | Situação da mensagem. |
contact | Nome e telefone de quem mandou. No Instagram e no Messenger, phone é null. |
created_at | Horário de Brasília, "YYYY-MM-DD HH:MM:SS". |
Com foto, áudio, vídeo ou documento, media traz o caminho para baixar o arquivo com a sua chave:
"message": {
"id": "1d9b2c3e-7a44-4f1d-8c2b-5e6f7a8b9c0d",
"external_id": "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABIYFkM4QjE",
"direction": "inbound",
"type": "image",
"content_type": null,
"text": "Segue o comprovante",
"media": { "mime_type": "image/jpeg", "download_path": "/api/v1/media/1d9b2c3e-7a44-4f1d-8c2b-5e6f7a8b9c0d" },
"status": "delivered",
"contact": { "name": "Maria Souza", "phone": "+5511988887777" },
"created_at": "2026-10-28 14:03:40"
}
GET https://chat.atalia.com.br/api/v1/media/{id} com o header x-api-key responde 302 para um link temporário (5 minutos). Se responder 409 MEDIA_PROCESSING, o arquivo ainda está sendo baixado: tente de novo em alguns segundos.
message.echo
Alguém da empresa respondeu pelo próprio app: o WhatsApp Business (na coexistência), o Instagram ou o Messenger. Mesmo formato do message.received, com direction: "outbound". Use para manter o histórico do seu sistema completo e, se fizer sentido, pausar a sua automação naquela conversa.
{
"id": "evt_0a1b2c3d4e5f6a7b8c9d0e1f",
"type": "message.echo",
"created_at": "2026-10-28T17:05:02.110Z",
"company_id": "6b1f2c9e-4d3a-4c55-9a51-3f7c1e2d9b10",
"client_id": null,
"channel": { "id": "ch_2f81a9c0d1", "type": "whatsapp", "phone": "+55 11 98765-4321" },
"data": {
"conversation_id": "48213",
"message": {
"id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"external_id": "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABEYEjQ5RkE",
"direction": "outbound",
"type": "text",
"content_type": null,
"text": "Oi, Maria! Aqui é a Carla, da loja.",
"media": null,
"status": "sent",
"contact": { "name": "Maria Souza", "phone": "+5511988887777" },
"created_at": "2026-10-28 14:05:01"
}
}
}
message.status
Uma mensagem enviada pela API mudou de situação. message_id é o id devolvido por POST /api/v1/messages.
{
"id": "evt_5d6e7f8a9b0c1d2e3f4a5b6c",
"type": "message.status",
"created_at": "2026-10-28T17:02:15.020Z",
"company_id": "6b1f2c9e-4d3a-4c55-9a51-3f7c1e2d9b10",
"client_id": null,
"channel": { "id": "ch_2f81a9c0d1", "type": "whatsapp", "phone": "+55 11 98765-4321" },
"data": {
"message_id": "6f1c2a7e-0b8e-4d0a-9a51-3f7c1e2d9b10",
"external_id": "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABEYEjI3QjE",
"status": "read",
"timestamp": "2026-10-28T17:02:14.000Z",
"errors": null
}
}
status é sent (aceita pela Meta), delivered (chegou no aparelho), read (lida) ou failed. Na falha, errors traz o motivo:
"data": {
"message_id": "6f1c2a7e-0b8e-4d0a-9a51-3f7c1e2d9b10",
"external_id": "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABEYEjI3QjE",
"status": "failed",
"timestamp": "2026-10-28T17:02:14.000Z",
"errors": [{ "code": 131047, "title": "Re-engagement message" }]
}
No Instagram e no Messenger chegam só delivered e read. Os status podem chegar fora de ordem: use o timestamp.
channel.connected
Uma caixa de entrada foi conectada. reconnected é true quando o canal já existia e foi conectado de novo (por exemplo, depois de trocar a senha do Facebook).
{
"id": "evt_1f2e3d4c5b6a7f8e9d0c1b2a",
"type": "channel.connected",
"created_at": "2026-10-28T16:40:00.000Z",
"company_id": "6b1f2c9e-4d3a-4c55-9a51-3f7c1e2d9b10",
"client_id": null,
"channel": { "id": "ch_2f81a9c0d1", "type": "whatsapp", "phone": "+55 11 98765-4321" },
"data": { "reconnected": false }
}
channel.disconnected
Uma caixa de entrada foi apagada (pelo painel ou por DELETE /api/v1/channels/{id}). Depois disso, o channel.id não existe mais. Ao apagar, a Atalia também desvincula o número (ou a conta) dela na Meta, para que ele possa ser conectado em outro lugar. unlinked diz como foi: ok, failed (fale com o suporte se precisar conectar o número em outro lugar) ou skipped (nada a desvincular, por exemplo porque outro canal ainda usa o número).
{
"id": "evt_2a3b4c5d6e7f8a9b0c1d2e3f",
"type": "channel.disconnected",
"created_at": "2026-10-29T12:00:00.000Z",
"company_id": "6b1f2c9e-4d3a-4c55-9a51-3f7c1e2d9b10",
"client_id": null,
"channel": { "id": "ch_2f81a9c0d1", "type": "whatsapp", "phone": "+55 11 98765-4321" },
"data": { "reason": "deleted", "unlinked": { "meta": "ok" } }
}
client.connected
Só para revenda. O cliente concluiu a página de conexão: aceitou os termos e conectou o primeiro canal. Chega uma vez por cliente. Os canais que ele conectar depois chegam como channel.connected, com o mesmo client_id.
{
"id": "evt_3c4d5e6f7a8b9c0d1e2f3a4b",
"type": "client.connected",
"created_at": "2026-10-28T18:10:00.000Z",
"company_id": "6b1f2c9e-4d3a-4c55-9a51-3f7c1e2d9b10",
"client_id": "c21a7f0e-2b4d-4e8a-9f1c-0d2e3f4a5b6c",
"channel": null,
"data": {
"client": { "id": "c21a7f0e-2b4d-4e8a-9f1c-0d2e3f4a5b6c", "name": "Clínica Bem-Estar", "email": "contato@clinicabemestar.com.br", "tax_id": "11222333000181" },
"channels": ["whatsapp"],
"ref": "usuario-8812"
}
}
ref é o que veio no link da página de conexão (?ref=), ou null. Use para saber qual usuário do seu sistema conectou. Guarde o client.id junto desse cadastro. Sem ref, case pelo tax_id (o CNPJ só com números) ou pelo email. Veja Conectar os números dos seus clientes.
client.no_slot
Só para revenda. O cliente tentou conectar e o seu pacote de canais estava cheio. Libere uma vaga (canal avulso ou pacote maior) e mande o link de novo.
{
"id": "evt_4d5e6f7a8b9c0d1e2f3a4b5c",
"type": "client.no_slot",
"created_at": "2026-10-28T18:12:00.000Z",
"company_id": "6b1f2c9e-4d3a-4c55-9a51-3f7c1e2d9b10",
"client_id": "c21a7f0e-2b4d-4e8a-9f1c-0d2e3f4a5b6c",
"channel": null,
"data": {
"client": { "id": "c21a7f0e-2b4d-4e8a-9f1c-0d2e3f4a5b6c", "name": "Clínica Bem-Estar", "email": "contato@clinicabemestar.com.br", "tax_id": "11222333000181" },
"ref": "usuario-8812"
}
}
ping
Teste enviado pelo botão "Enviar mensagem de teste" ou por POST /api/v1/webhook/test. Responda 2xx e ignore.
{
"id": "evt_5e6f7a8b9c0d1e2f3a4b5c6d",
"type": "ping",
"created_at": "2026-10-28T16:00:00.000Z",
"company_id": "6b1f2c9e-4d3a-4c55-9a51-3f7c1e2d9b10",
"client_id": null,
"channel": null,
"data": { "message": "Teste do webhook da Atalia" }
}
Verificar a assinatura
X-Atalia-Signature: t=<epoch em segundos>,v1=<hex>, onde v1 é o HMAC-SHA256 de "<t>.<corpo cru>" com o segredo whsec_... que aparece em Chave e webhook › Segredo da assinatura. Calcule sobre o corpo CRU, antes de ler o JSON, e recuse t com mais de 5 minutos.
import crypto from 'node:crypto';
function verifyAtaliaSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ''));
}
import hashlib, hmac, time
def verify_atalia_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Tentativas, repetidos e ordem
- Responda
2xxem até 10 segundos. Grave o evento e processe depois. Qualquer outra resposta, ou demora, conta como falha. Redirecionamentos não são seguidos. - Falhou, a Atalia tenta de novo, até 9 tentativas no total, com espera crescente: 30 s, 1 min, 2 min, 4 min, 8 min, 16 min, 32 min e 1 h (cerca de 2 horas no total). Depois disso, o evento fica como "Falhou" em Entregas do webhook, e você pode reenviar de lá.
- O mesmo evento pode chegar mais de uma vez (tentativa depois de um timeout, ou reenvio pelo painel). Ele chega com o mesmo
id: guarde os ids recebidos e ignore os repetidos. - A ordem não é garantida. Use o
created_atdo envelope e otimestampdomessage.status.
Entregas do webhook
No painel, Entregas do webhook lista os eventos dos últimos 7 dias: o que foi entregue, o que está sendo tentado de novo e o que falhou. Em cada um aparecem o JSON enviado, a resposta do seu sistema (o começo dela), o código HTTP, o tempo e o erro. O botão Reenviar manda o mesmo evento outra vez, com o mesmo id e uma assinatura nova.
Responder
Para responder um message.received, envie pelo conversation_id que chegou no evento:
curl -X POST https://chat.atalia.com.br/api/v1/messages \
-H "x-api-key: $ATALIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reply-6f1c2a7e-0b8e-4d0a-9a51-3f7c1e2d9b10" \
-d '{"channel_id":"ch_2f81a9c0d1","conversation_id":"48213","type":"text","text":"Entregamos sim!"}'
Um sistema completo que recebe e responde estes eventos está no projeto de exemplo. A referência de todas as rotas está em llms.txt.