Pular para o conteúdo principal

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 à parte

O 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.

EventoQuando dispara
message.receivedMensagem recebida (inbound) na instância
message.sentMensagem enviada a partir da própria sessão (inclui envios feitos direto no WhatsApp, não só pela API)
message.ackConfirmação de entrega/leitura de uma mensagem enviada
qrcode.updatedNovo QR code gerado para pareamento
connection.updateMudança de status da sessão (conectando, pronta, desconectada)
group.participants.updateParticipante entrou, saiu, ou o grupo foi atualizado
call.receivedChamada recebida na sessão do WhatsApp
instance.healthKill 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 mensagem

O 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).

O campo attempts de cada linha é sempre 1

Isso é 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.

Entrega real sob carga ainda não foi observada em produção

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).