Webhooks
A API pode notificar seu backend em tempo real quando algo acontece numa
instância — mensagem recebida, confirmação de leitura, QR code novo,
mudança de status, saúde da instância caindo para vermelho, etc. Cada
evento gera um POST HTTP para a URL configurada, com retry automático em
caso de falha.
Se você prefere consumir os eventos direto no seu processo (sem expor um endpoint HTTP), veja WebSocket — ambos os canais recebem exatamente os mesmos eventos, com o mesmo formato de payload.
Configuração
Por instância
PATCH /instance/:name/webhook define a URL e os eventos assinados desta
instância:
curl -X PATCH http://localhost:8080/instance/minha-instancia/webhook \
-H "apikey: SUA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://meu-backend.com/webhooks/whatsapp",
"webhookEvents": ["message.received", "message.ack", "instance.health"]
}'
GET /instance/:name/webhook consulta a configuração atual ({ webhookUrl, webhookEvents }). Passar webhookUrl: null no PATCH limpa a URL
cadastrada. A URL também pode ser definida já na criação da instância
(POST /instance/create, mesmos dois campos no corpo).
webhookEvents é uma allow-list: só aceita os 8 nomes de evento válidos
(lista abaixo) — qualquer outro valor é rejeitado com 400. Se
webhookEvents vier vazio (ou não for definido), a instância recebe TODOS
os eventos — o filtro só entra em ação quando a lista tem pelo menos um
item.
Fallback global
Se a instância não tiver webhookUrl própria, a API usa a variável de
ambiente WEBHOOK_GLOBAL_URL (quando configurada) como destino. É útil
para operar várias instâncias contra o mesmo endpoint sem repetir a
configuração em cada uma. O filtro de webhookEvents da instância continua
valendo normalmente sobre esse fallback.
instance.health é um caso à parteO evento instance.health (kill switch, ver abaixo) não usa o fallback
global e não respeita o filtro webhookEvents — ele só é entregue quando
a instância tem webhookUrl própria configurada, e nesse caso é sempre
entregue, mesmo que instance.health não esteja na lista de eventos
assinados. Isso é o que o código faz hoje (src/schedulers/index.ts); se
você depende do kill switch, garanta que a instância tenha webhookUrl
própria — o fallback global não cobre esse evento.
Os 8 eventos
Os nomes abaixo são exatamente os do EVENT_MAP/WEBHOOK_EVENTS em
src/ws/index.ts — a mesma allow-list usada para validar webhookEvents
no PATCH acima.
| Evento | Quando dispara |
|---|---|
message.received | Mensagem recebida (inbound) na instância |
message.sent | Mensagem enviada a partir da própria sessão (inclui envios feitos direto no WhatsApp, não só pela API) |
message.ack | Confirmação de entrega/leitura de uma mensagem enviada |
qrcode.updated | Novo QR code gerado para pareamento |
connection.update | Mudança de status da sessão (conectando, pronta, desconectada) |
group.participants.update | Participante entrou, saiu, ou o grupo foi atualizado |
call.received | Chamada recebida na sessão do WhatsApp |
instance.health | Kill switch — a instância acabou de entrar em estado vermelho |
Formato do payload
Todo POST de webhook (e toda mensagem de WebSocket) usa o mesmo envelope,
montado por buildPayload (src/services/WebhookService.ts):
{
"instance": "minha-instancia",
"event": "message.received",
"timestamp": "2026-07-15T14:32:10.123Z",
"data": { }
}
O conteúdo de data depende do evento — é o resultado de serializeArgs
(src/ws/index.ts), que extrai só um subconjunto seguro dos dados brutos
do whatsapp-web.js.
message.received e message.sent
{
"instance": "minha-instancia",
"event": "message.received",
"timestamp": "2026-07-15T14:32:10.123Z",
"data": {
"id": "true_5565999998888@c.us_3EB0...",
"from": "5565999998888@c.us",
"to": "5511888887777@c.us",
"body": "Oi, ainda tem esse produto?",
"type": "chat",
"timestamp": 1752590000,
"fromMe": false
}
}
message.sent tem exatamente o mesmo formato de data (mesma função de
serialização), só com fromMe: true e disparado a partir do evento
message_create do whatsapp-web.js.
data.body contém o texto da mensagemO payload de message.received (e message.sent) carrega o corpo da
mensagem dentro de data.body. Trate a URL do seu webhook como dado
sensível: use HTTPS, valide a origem se possível, e não logue o payload
completo em texto plano num sistema sem controle de acesso — é conteúdo de
conversa real do seu cliente.
message.ack
{
"instance": "minha-instancia",
"event": "message.ack",
"timestamp": "2026-07-15T14:32:15.400Z",
"data": { "id": "true_5565999998888@c.us_3EB0...", "ack": 3 }
}
ack segue a numeração do whatsapp-web.js: 3 = entregue, 4 = lido
(são os dois valores que a própria API usa internamente para incrementar
os contadores delivered/read do AntiBanGuard).
qrcode.updated
{
"instance": "minha-instancia",
"event": "qrcode.updated",
"timestamp": "2026-07-15T14:30:00.000Z",
"data": { "qr": "2@AbCdEf...,base64..." }
}
data.qr é a string bruta do QR code (não a data URL PNG que
GET /instance/connect/:name devolve) — para renderizar visualmente, gere
o PNG você mesmo a partir dessa string (ex.: lib qrcode no Node).
connection.update
{
"instance": "minha-instancia",
"event": "connection.update",
"timestamp": "2026-07-15T14:31:00.000Z",
"data": { "status": "connected" }
}
Disparado nas transições status, ready e disconnected do
WhatsAppManager — todas mapeiam para este mesmo nome de evento público.
group.participants.update e call.received
Estes dois eventos ainda não têm um extrator dedicado em serializeArgs —
caem no caso genérico da função, que devolve { "raw": true } como
data. Ou seja, hoje o payload avisa que o evento aconteceu, mas não
traz detalhes estruturados (quem entrou/saiu do grupo, quem ligou, etc.).
Se sua integração depende desses detalhes, hoje é preciso consultar as
rotas REST de grupo (ver Referência da API) depois de
receber o evento.
instance.health
{
"instance": "minha-instancia",
"event": "instance.health",
"timestamp": "2026-07-15T14:35:00.000Z",
"data": { "healthState": "red" }
}
Disparado pelo scheduler de saúde (roda a cada 5 minutos,
src/schedulers/index.ts) somente na transição para vermelho (estado
anterior diferente de red, novo estado igual a red) — não repete a
cada rodada enquanto a instância permanece vermelha. É o kill switch:
quando a instância entra em vermelho, o AntiBanGuard já pausa os envios
proativos sozinho; este evento é o aviso para o seu sistema também reagir
(alertar um operador, pausar campanhas, etc.).
Retry e log de entregas
A entrega roda numa fila BullMQ (src/core/queues/webhookQueue.ts), fora
do caminho da requisição HTTP que gerou o evento. Configuração real do
job:
{ attempts: 3, backoff: { type: 'exponential', delay: 2000 } }
Ou seja: até 3 tentativas no total, com backoff exponencial a partir de
2000ms — a fórmula padrão do BullMQ (delay * 2^(tentativa - 1)) dá um
intervalo de 2s antes da 2ª tentativa e 4s antes da 3ª (só 2
intervalos, já que são 3 tentativas ao todo, não 3 esperas). Cada POST
tem timeout de 10 segundos (WEBHOOK_TIMEOUT_MS); qualquer resposta fora
da faixa 2xx, erro de rede, timeout, ou URL bloqueada pela guarda de SSRF
conta como falha e aciona o retry.
Cada tentativa individual (não só o resultado final) grava uma linha em
WebhookDelivery, consultável por:
GET /instance/:name/webhooks/deliveries?limit=50
Resposta: lista de { id, event, url, status, attempts, lastError, payload, createdAt, ... }, mais recente primeiro (limit padrão 50, máximo 200).
attempts de cada linha é sempre 1Isso é uma particularidade real do código: cada execução do job (cada
tentativa do BullMQ) chama WebhookService.deliver, que sempre cria uma
nova linha de WebhookDelivery com attempts: 1 — o campo não é
incrementado numa linha existente. Para saber quantas tentativas um evento
específico levou, conte quantas linhas com o mesmo event/url aparecem
próximas no tempo (mesmo createdAt, com poucos segundos de diferença),
em vez de olhar attempts numa única linha.
O payload retornado por este endpoint é o mesmo enviado ao seu webhook —
inclui a mesma ressalva de message.received/message.sent sobre conter
texto de mensagem.
O comportamento acima (retry, backoff, gravação de WebhookDelivery) é o
que o código faz — mas a entrega de webhooks sob volume real de mensagens,
endpoints lentos ou instáveis do lado do cliente, e o comportamento do
BullMQ sob essas condições, ainda não foi validado ponta a ponta em
produção neste projeto. Se você notar entregas fora de ordem, duplicadas
(ex.: uma tentativa que teve sucesso do lado do seu servidor mas timeout do
lado da API antes de receber a resposta) ou atrasadas, isso é esperado de
um sistema de fila com retry — seu endpoint de webhook deve ser
idempotente (tratar o mesmo evento chegando mais de uma vez).