Eventos webhook
Hub emite 10 eventos webhook. Esta pagina documenta cada um. Pra contrato geral (HMAC, retry), ver Webhook contract.
Eventos de mensagem
message.received
Disparado pra cada mensagem nova recebida pelo chip (ou enviada manualmente do celular pareado, se receive_self_chat=true).
{ "event": "message.received", "timestamp": "2026-04-30T12:34:56.789Z", "data": { "message_id": "01HZTQMSG...", "chip_id": "01HZTQCHIP...", "wa_message_id": "BAE5...", "from": "5511988887777@s.whatsapp.net", "to": "5511999998888:30@s.whatsapp.net", "direction": "in", "type": "text", "content": { "text": "ola" }, "timestamp": "2026-04-30T12:34:55.000Z", "sender": { "jid": "5511988887777@s.whatsapp.net", "name": "Maria", "phone_number": "5511988887777", "lid": "12345678901234@lid", "is_business": false, "verified_business_name": null, "photo_media_id": "01HZTQPHOTO..." }, "chat": { "jid": "5511988887777@s.whatsapp.net", "is_group": false, "name": null, "participants": null, "participants_count": null, "photo_media_id": "01HZTQPHOTO..." } }}Campos chave
| Campo | Notas |
|---|---|
direction | 'in' recebida; 'out' enviada do celular pareado (precisa opt-in) |
sender.jid | JID do humano que enviou (em grupo == participant, em 1-1 == from) |
sender.name | pushName (editavel pelo proprio remetente) |
sender.phone_number | telefone “puro” (so digitos). Vazio quando WA nao expoe |
sender.lid | LID alternate quando WA expoe — cross-match LID↔telefone |
sender.is_business | true se conta WhatsApp Business verificada |
sender.verified_business_name | nome verificado pelo WA (null em conta pessoal) |
sender.photo_media_id | foto de perfil cacheada do remetente (null se sem foto/bloqueada). Baixe via GET /v1/media/:id com Bearer da app. Em 1:1 inbound, igual a chat.photo_media_id. |
chat.is_group | derivado de chat.jid terminar em @g.us |
chat.participants | so em grupo: [{jid, admin: 'admin'|'superadmin'|null}] |
chat.photo_media_id | foto do chat — em grupo e a foto do grupo, em 1:1 e a do contato. Mesma regra de download. |
Cacheando fotos de perfil
photo_media_id muda quando o contato/grupo troca a foto. Cache pelo id: se voce ja tem o binario, pula o download. Foto desbloqueia o lazy-fetch via Baileys no primeiro acesso, depois fica no MinIO ate o WA notificar mudanca.
Tipos de content por type
// type: text{ "text": "..." }
// type: image / audio / document / video{ "mimetype": "...", "media_id": "01HZTQ...", "caption": "...", ... }
// type: location{ "lat": -23.5505, "lng": -46.6333, "name": "...", "address": "..." }
// type: reaction{ "emoji": "❤️", "to_wa_message_id": "BAE5..." }
// type: unknown (protocolo Baileys que nao sabemos parsear){ "kind": "..." }message.status
Mudanca de status de mensagem outbound (sent → delivered → read → failed).
{ "event": "message.status", "timestamp": "...", "data": { "message_id": "01HZTQMSG...", "chip_id": "01HZTQCHIP...", "wa_message_id": "BAE5...", "status": "delivered" }}message.transcribed
Audio transcrito (so pra apps com transcribe_audio_mode != 'off').
{ "event": "message.transcribed", "timestamp": "...", "data": { "message_id": "01HZTQMSG...", "chip_id": "01HZTQCHIP...", "status": "success", "transcription": { "text": "ola, queria confirmar meu pedido", "language": "pt", "duration_seconds": 8.5, "cost_cents": 0.085, "provider": "openai", "model": "whisper-1" }, "error": null }}Em falha: status: "failed", transcription: null, error: "...".
Eventos de chip
chip.connected
Chip pareou ou reconectou e o socket Baileys esta open.
{ "event": "chip.connected", "timestamp": "...", "data": { "chip_id": "01HZTQCHIP...", "phone_number": "5511999998888" }}chip.disconnected
⚠️ So em close TERMINAL (apos badSession streak ou loggedOut/multidevice/replaced). Closes transient (badSession recuperavel, connectionLost, etc) nao disparam webhook — recuperam em segundos via reconnect automatico.
{ "event": "chip.disconnected", "timestamp": "...", "data": { "chip_id": "01HZTQCHIP...", "reason": "loggedOut", "will_reconnect": false, "is_terminal": true, "status_code": 401, "consecutive_bad_session": 0 }}chip.logged_out
Chip foi marcado como logged_out (sessao Baileys destruida, precisa re-parear). Disparado junto com chip.disconnected mas separado pra app reagir especificamente.
{ "event": "chip.logged_out", "timestamp": "...", "data": { "chip_id": "01HZTQCHIP..." }}chip.banned
Detector automatico de ban marcou chip como banned. Critico — fila pausada, requer intervencao manual.
{ "event": "chip.banned", "timestamp": "...", "data": { "chip_id": "01HZTQCHIP...", "reason": "401 persistent + 0 inbound 1h" }}chip.qr_loop
Chip ficou em pairing por mais que 5min — sintoma de QR esquecido. Re-notifica a cada 30min ate 24h, depois envia 1x final e para.
{ "event": "chip.qr_loop", "timestamp": "...", "data": { "chip_id": "01HZTQCHIP...", "pairing_for_minutes": 7 }}chip.bad_session_streak
Preventivo. Disparado quando consecutiveBadSession==2 (penultimo close antes de virar terminal real). Avisa antes de cair.
{ "event": "chip.bad_session_streak", "timestamp": "...", "data": { "chip_id": "01HZTQCHIP...", "streak": 2 }}chip.silent
Chip esta connected mas socket Baileys parou de receber eventos (possivel “fantasma”). Detector passivo dispara apos >2h sem connection.open/connection.close/inbound. Re-emite a cada 1h ate 6h, depois para. Mesmos requisitos de app_chip_access.canReceive que outros eventos de chip.
{ "event": "chip.silent", "timestamp": "2026-05-03T10:30:00.000Z", "data": { "chip_id": "01HZTQCHIP...", "silent_for_minutes": 145 }}Eventos de grupo (continuacao)
group.left
Chip saiu OU foi removido do grupo. Detectado em group.member_removed quando o JID do chip aparece nos participants removidos.
{ "event": "group.left", "timestamp": "2026-05-03T11:00:00.000Z", "data": { "chip_id": "01HZTQCHIP...", "group_jid": "120363xxx@g.us", "reason": "left", "performer_jid": "5511988887777@s.whatsapp.net" }}| Campo | Notas |
|---|---|
reason | 'left' quando o proprio chip saiu (o performer e o chip ou null); 'removed' quando outro admin removeu |
performer_jid | quem performou a remocao (null se nao exposto pelo Baileys) |
Eventos de contato
contact.updated
Disparado quando um contato sincronizado muda — entrada nova, edicao de nome/notify ou troca de foto. Apos uma janela de debounce de 3s por (chip_id, jid), o hub coalesca multiplos eventos do mesmo contato em 1 webhook so. Util pra o consumidor reaplicar cache.
{ "event": "contact.updated", "timestamp": "2026-05-03T16:45:00.000Z", "data": { "chip_id": "01HZTQCHIP...", "contact": { "jid": "5511988887777@s.whatsapp.net", "lid": "12345678901234@lid", "phone_number": "5511988887777", "name": "Maria Silva", "notify": "Maria", "verified_name": null, "photo_media_id": "01HZTQPHOTO...", "removed_at": null }, "reasons": ["update", "photo"] }}| Campo | Notas |
|---|---|
reasons | array com 1+ de: upsert (contato novo), update (nome/notify mudou), photo (foto trocou) |
contact.photo_media_id | id da foto atual cacheada — baixe via GET /v1/media/:id. Mesma logica do messages.received |
contact.removed_at | nao-null quando contato foi removido da agenda |
Em sync inicial (
messaging-history.setapos pareamento), pode chegar uma rajada. O debounce reduz, mas espere centenas de eventos no primeiro minuto pos-pareamento. Limite o processamento se necessario.
Resumo — quando reagir
| Evento | Acao recomendada |
|---|---|
message.received | processa mensagem; se chatbot, responda |
message.status | atualiza status na sua UI |
message.transcribed | salva texto da transcricao |
chip.connected | atualiza UI (“chip ativo”) |
chip.disconnected | alerta operador: chip caiu terminal |
chip.logged_out | alerta CRITICO: precisa re-parear |
chip.banned | alerta CRITICO: troca de chip imediato |
chip.qr_loop | lembrete: alguem esqueceu QR |
chip.bad_session_streak | atencao: chip pode cair em breve |
chip.silent | investigar: chip parece fantasma |
contact.updated | atualiza cache local do contato (foto/nome) |
group.left | remove grupo do estado da app (chip nao recebe mais mensagens dele) |
Como sua app deve listar eventos que escuta
Hoje hub manda todos os eventos pra app que tem can_receive=true no chip. Filtro por tipo de evento por app esta em backlog (P3). Ate la, ignore os que nao usa.
Versionamento de eventos
Adicao de campo nao-breaking entra direto. Renomear/remover campo virara versao nova do evento (message.received.v2) ou novo evento. Hoje todos os eventos sao v1 implicito.