Webhooks

15. Webhooks#

Webhooks permitem receber eventos em tempo real via HTTP POST no seu servidor.

Semantica de entrega: pelo menos uma vez (at-least-once). Retries do worker, falhas de rede ou retry manual podem gerar o mesmo event_id mais de uma vez. O endpoint do cliente deve ser idempotente e deduplicar por event_id (ou pelo par webhook_id + event_id).

Ordem em relação ao banco (tenant): para message.delivered, message.read, message.played, message.deleted e message.edited, o Catcher aplica primeiro o UPDATE na tabela messages do tenant e em seguida enfileira/dispara webhooks e streams em tempo real — o estado persistido não fica atrás do envio do evento HTTP.

POST/v1/webhooks#

Cria um novo webhook.

Auth: Owner, Admin

Request:

json
{
  "url": "https://meuservidor.com/webhook",
  "instance_id": "84c2e480-...",
  "events": "message.received,message.sent",
  "secret": "whsec_minha_chave_compartilhada"
}
Campo Tipo Obrigatório Descricao
url string sim URL do seu endpoint (https:// por padrão)
instance_id string não ID da instância para filtrar. Vazio/omitido = global (recebe de todas as instâncias da empresa)
events string sim Eventos filtrados (separados por virgula). * = todos
secret string não Secret customizado. Se omitido, a API gera um automaticamente

Escopo do webhook — por instância vs global:

  • Por instância: defina instance_id para receber eventos apenas daquela instância. Recomendado quando cada instância envia para um backend diferente.
  • Global: omita instance_id (ou envie vazio) para receber eventos de todas as instâncias da empresa no mesmo endpoint.
  • Cuidado com duplicacao: se você criar um webhook global E um por instância apontando para o mesmo URL, os eventos daquela instância serao entregues duas vezes (uma por cada webhook). Prefira um ou outro.

Eventos disponíveis:

Evento Descricao Persistido
message.received Mensagem recebida Sim (tabela messages)
message.sent Mensagem enviada com sucesso Sim (tabela messages)
message.delivered Mensagem entregue (double check) Sim (atualiza delivered_at)
message.read Mensagem lida (blue check) Sim (atualiza read_at)
message.played Mídia visualizar-uma-vez aberta Sim (atualiza played_at)
message.deleted Mensagem apagada/revogada Sim (atualiza status=deleted)
message.edited Mensagem editada Sim (atualiza content)
message.undecryptable Mensagem recebida mas não pode ser decriptada Sim (tabela messages, status=undecryptable)
message.starred Mensagem marcada/desmarcada com estrela Não
message.deleted_for_me Mensagem apagada localmente (apenas para mim) Não
call.received Chamada recebida Sim (tabela call_logs)
call.accepted Chamada aceita pela parte remota Sim (tabela call_logs)
call.rejected Chamada rejeitada pela parte remota Sim (tabela call_logs)
call.ended Chamada finalizada Sim (tabela call_logs)
call.group_offer Chamada em grupo recebida Sim (tabela call_logs)
connection.update Mudanca de status da conexão Sim (tabela instances)
presence.update Presença ou digitacao de contato Não em MySQL; snapshot opcional em Redis quando o contato está monitorado
group.update Mudanca em grupo Sim (tabela group_events)
group.joined Instância foi adicionada a um grupo Sim (tabela group_events)
contact.update Mudanca de contato (foto, nome, etc.) Sim (tabela contact_events)
contact.identity_changed Contato trocou dispositivo principal Sim (tabela contact_events)
contact.sync Contato sincronizado do dispositivo Sim (tabela contact_events)
instance.banned Instância recebeu ban do WhatsApp Sim (atualiza ban_expiry e ban_reason na instância)
instance.offline Instância desconectada ha 15min ou mais (transicao única, não re-fira) Sim (campo offline_alert_level=warning na instância)
instance.critical_offline Instância desconectada ha 6h ou mais; também dispara email para o owner + superadmins Sim (campo offline_alert_level=critical na instância)
instance.recovered Instância voltou a conectar após ter ficado offline/critical_offline Sim (limpa offline_alert_level)
blocklist.update Lista de bloqueados alterada Não
chat.pin Chat fixado/desfixado Não
chat.archive Chat arquivado/desarquivado Não
chat.mute Chat silenciado/dessilenciado Não
chat.read_state Chat marcado como lido/não lido Não
chat.clear Chat limpo Não
chat.delete Chat apagado Não
privacy.update Configurações de privacidade alteradas Não
history.sync Sincronização de histórico recebida Não
sync.preview Preview de sincronização offline Não
media.retry_result Resultado de retry de mídia Não
label.edit Etiqueta criada/editada/removida Não
label.chat_association Chat etiquetado/desetiquetado Não
label.message_association Mensagem etiquetada/desetiquetada Não
message.acked Outro dispositivo SEU confirmou recebimento de uma mensagem que você enviou (sync multi-device; distinto de message.delivered, que e o contato) Não
message.read_other_device Você leu uma mensagem inbound em outro dispositivo seu Não
connection.diagnostic Timeline fina do ciclo de conexão (dialing, qr emitido, ws fechado, etc.) — usado pela modal de QR Não
contacts.bulk_imported Roster de contatos absorvido em massa (após pareamento novo) Não
sync.offline_completed whatsmeow terminou de absorver eventos perdidos numa janela de Disconnected Não
connection.recovered_quick Connected logo após um Disconnected (<~60s); ruido de estabilidade, distinto de instance.recovered Não
* Todos os eventos

Resposta 201:

json
{
  "id": 1,
  "url": "https://meuservidor.com/webhook",
  "secret": "a1b2c3d4e5f6...64_hex_chars",
  "events": "message.received,message.sent",
  "active": true
}

O secret e gerado automaticamente e usado para assinar os payloads via HMAC-SHA256. A URL e validada no cadastro/edicao e revalidada no momento da entrega; destinos internos ou resolvidos para rede privada são rejeitados. O secret aparece somente na resposta de criação. GET, LIST e PATCH nunca o reexibem. A entrega e https only por padrão. Em runtime você pode afrouxar isso com WEBHOOK_ALLOW_INSECURE_HTTP=true ou restringir dominios com WEBHOOK_ALLOWED_DOMAINS / WEBHOOK_BLOCKED_DOMAINS.

Email verificado obrigatório: o cadastro de webhook exige que a conta já tenha confirmado o email — uma conta recem-criada e ainda não verificada recebe 403.

Respostas de erro:

Status error_code Quando
400 INVALID_JSON Corpo não e JSON válido
400 BAD_REQUEST url ou events ausente; ou secret informado com menos de 16 caracteres
400 WEBHOOK_URL_INVALID URL inválida — HTTP em produção, scheme não suportado ou destino que resolve para rede privada (SSRF)
403 FORBIDDEN Limite de webhooks do plano atingido
404 INSTANCE_NOT_FOUND instance_id informado não corresponde a nenhuma instância da empresa
409 CONFLICT Já existe um webhook para a mesma empresa + instância + URL

GET/v1/webhooks#

Lista todos os webhooks da empresa.

Auth: Owner, Admin

Resposta 200:

json
[
  {
    "id": 1,
    "url": "https://meuservidor.com/webhook",
    "events": "message.received,message.sent",
    "active": true,
    "created_at": "2026-03-07T10:00:00Z"
  }
]

GET/v1/webhooks/{webhookId}#

Retorna detalhes de um webhook.

Auth: Owner, Admin (superadmin tem acesso cross-company — ver "Webhook detail endpoints" no fim da seção de feeds)

Resposta 200: Objeto webhook sem o campo secret.


PATCH/v1/webhooks/{webhookId}#

Atualiza um webhook.

Auth: Owner, Admin (superadmin tem acesso cross-company — ver "Webhook detail endpoints" no fim da seção de feeds)

Request:

json
{
  "url": "https://novo-servidor.com/webhook",
  "events": "*",
  "active": false,
  "secret": "whsec_rotacionado_manual"
}

Todos os campos são opcionais.

Resposta 200: Objeto webhook atualizado sem o campo secret.


DELETE/v1/webhooks/{webhookId}#

Remove um webhook.

Auth: Owner, Admin (superadmin tem acesso cross-company — ver "Webhook detail endpoints" no fim da seção de feeds)

Resposta 204: Sem corpo.


GET/v1/webhooks/{webhookId}/logs#

Lista logs de entrega do webhook.

Retencao e privacidade: registros de entrega com sucesso são removidos após 7 dias; falhas permanecem 14 dias (tarefa periodica webhook:cleanup_logs). Em cada execução, linhas com mais de 48 horas passam por redacao: o campo payload da API vira um JSON enxuto com event_type, event_id e redacted: true; copia completa do corpo enviado ao cliente e apagada no armazenamento interno; respostas HTTP muito longas são truncadas. Novas linhas já gravam em payload apenas metadados (tipo, id, tamanho do JSON). O retry manual depende dessa copia original: quando payload_raw já tiver sido limpo, o reenvio não e mais permitido.

Auth: Owner, Admin (superadmin tem acesso cross-company — ver "Webhook detail endpoints" no fim da seção de feeds)

Query Parameters:

Parametro Tipo Padrão Descricao
page int 1 Página
limit int 50 Itens por página (max 100)

Resposta 200:

json
{
  "data": [
    {
      "id": 1,
      "webhook_id": 1,
      "event_id": "evt-abc-123",
      "event_type": "message.received",
      "payload": "{...}",
      "payload_raw": "{...}",
      "status_code": 200,
      "response": "OK",
      "success": true,
      "attempt": 1,
      "error_class": "",
      "created_at": "2026-03-07T12:00:00Z"
    }
  ],
  "total": 25,
  "page": 1,
  "limit": 50
}

Erro 404 (WEBHOOK_NOT_FOUND): webhook inexistente ou de outra empresa.


POST/v1/webhooks/{webhookId}/logs/{logId}/retry#

Reenvia um evento que falhou.

Auth: Owner, Admin (superadmin tem acesso cross-company — ver "Webhook detail endpoints" no fim da seção de feeds) Regras: so aceita logs com success=false e com payload_raw ainda preservado. Logs já entregues com sucesso ou já redatados retornam 409 Conflict.

Resposta 202:

json
{
  "status": "retrying",
  "event_id": "evt_xxxx"
}

GET/v1/webhooks/stuck#

Lista entregas webhook falhadas que precisam de atenção, agregadas por event_id (uma linha por evento, com a contagem total de tentativas e a classe do erro).

Auth: Owner, Admin Escopo: somente webhooks da company autenticada.

Query params (todos opcionais):

Param Tipo Default Descricao
webhook_id int Filtra por webhook especifico
instance_id string Estreita para webhooks dessa instância OU webhooks globais (sem instance_id, que disparam para qualquer instância). Usado pela aba Webhooks da página de detalhe da instância.
event_type string Filtra por tipo de evento (ex: message.received)
class string Filtra por classe do erro: permanent_4xx, transient_5xx, rate_limit, network, unknown
since RFC3339 now-24h Cutoff inferior
page int 1 Página
limit int 50 Itens por página (max 200)

Resposta 200:

json
{
  "data": [
    {
      "log_id": 123,
      "webhook_id": 7,
      "webhook_url": "https://app.exemplo.com/webhook",
      "event_id": "evt-abc-123",
      "event_type": "message.received",
      "status_code": 503,
      "response": "service unavailable",
      "attempts": 25,
      "error_class": "transient_5xx",
      "is_hard_failed": false,
      "has_payload_raw": true,
      "created_at": "2026-04-08T15:30:45Z"
    }
  ],
  "total": 12,
  "page": 1,
  "limit": 50
}

is_hard_failed=true indica que o evento bateu em SkipRetry por ser permanent_4xx após 3 tentativas — provavelmente um bug de configuração (URL errada, secret expirado, parser quebrado). has_payload_raw=false indica que o payload_raw foi redatado ou nunca foi salvo, e não da pra reenviar via API.


POST/v1/webhooks/retry-bulk#

Re-enfileira em batch um conjunto de entregas falhadas. Aceita dois modos:

Modo 1 — explicito por log IDs:

json
{
  "log_ids": [123, 124, 125]
}

Modo 2 — por filtro:

json
{
  "webhook_id": 7,
  "event_type": "message.received",
  "class": "transient_5xx",
  "since": "2026-04-07T00:00:00Z"
}

Auth: Owner, Admin Escopo: somente webhooks da company autenticada (logs de outros tenants são silenciosamente pulados). Limites: máximo de 500 retries por chamada. Eventos com payload_raw vazio são pulados. Re-enfileiramento e deduplicado por event_id.

Resposta 202:

json
{
  "requeued": 7,
  "skipped": 1,
  "errors": ["log 199: payload unavailable"]
}

Estrategia de retry e classificação de erros#

Quando uma entrega falha, o Catcher classifica o erro e aplica a estrategia de retry adequada:

Classe Códigos Estrategia
permanent_4xx 400, 401, 403, 404, 410, 422 Hard-fail após 3 tentativas — vai pra DLQ. Quase sempre indica URL errada, secret expirado ou bug no parser do consumer. Aparece como "Eventos travados" na UI com badge vermelho.
rate_limit 408, 429 Retry pelo schedule completo.
transient_5xx 500-599 Retry pelo schedule completo.
network sem status (DNS, refused, timeout) Retry pelo schedule completo. Caso típico: consumer reiniciando.
unknown qualquer outro Retry pelo schedule completo.

Schedule de retry (default WEBHOOK_MAX_RETRY=30, configuravel por env):

text
n=1   → 5s
n=2   → 15s
n=3   → 30s
n=4   → 1min
n=5   → 2min
n=6   → 5min
n=7   → 10min
n=8   → 15min
n=9   → 30min
n>=10 → 1h (capped)

Janela total: ~22 horas. Cobre restart de serviço, manutenção planejada, picos de carga e blips de infraestrutura sem perder eventos.

Wake-up retry on success: quando uma entrega chega com sucesso a um webhook, o Catcher automaticamente re-enfileira todos os eventos falhados (excluindo permanent_4xx) desse mesmo webhook nas últimas 24h. Isso acelera a recuperacao quando o consumer volta de um restart — você não precisa esperar o backoff exponencial drainar. Um Redis lock por webhook (60s TTL) evita thundering herd.

Persistencia de payload_raw: entregas falhadas mantem o payload original não redatado pelo periodo de retencao completo (14 dias), de modo que retry manual via UI ou via POST /v1/webhooks/retry-bulk continue funcionando para eventos antigos.


Verificação de assinatura HMAC#

Cada requisição webhook inclui X-Catcher-Timestamp e X-Catcher-Signature. A assinatura HMAC-SHA256 cobre a string timestamp + "." + raw_body, usando o secret do webhook.

Validação recomendada no receiver:

  • rejeite timestamps com skew maior que 5 minutos
  • use comparacao constant-time (hmac.compare_digest, crypto.timingSafeEqual, subtle.ConstantTimeCompare)
  • deduplicate por event_id porque a entrega e at-least-once

Headers enviados pelo Catcher:

text
Content-Type: application/json
User-Agent: Catcher-Webhook/1.0
X-Catcher-Timestamp: 1711035600
X-Catcher-Signature: sha256=<hmac_hex>

Validação (exemplo em Python):

python
import hmac
import hashlib
import time

def verify_signature(payload_body, secret, timestamp, signature_header):
    now = int(time.time())
    ts = int(timestamp)
    if abs(now - ts) > 300:
        return False
    signed = f"{timestamp}.".encode() + payload_body
    expected = "sha256=" + hmac.new(
        secret.encode(), signed, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Validação (exemplo em Node.js):

javascript
const crypto = require('crypto');

function verifySignature(payloadBody, secret, timestamp, signatureHeader) {
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(timestamp)) > 300) {
    return false;
  }
  const signed = Buffer.concat([
    Buffer.from(`${timestamp}.`),
    Buffer.isBuffer(payloadBody) ? payloadBody : Buffer.from(payloadBody)
  ]);
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(signed)
    .digest('hex');
  if (expected.length !== signatureHeader.length) {
    return false;
  }
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

Payloads dos eventos#

Todos os eventos seguem a estrutura base:

json
{
  "event_id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "message.received",
  "instance_id": "84c2e480-...",
  "timestamp": "2026-03-07T15:30:45.123Z",
  "data": { ... }
}
Campo do envelope Tipo Observacoes
event_id string (UUID v4) Identificador único da entrega. Use para deduplicacao idempotente no receiver.
type string Nome do evento (message.received, connection.update, etc.). Este e o nome do campo — não e event.
instance_id string (UUID) A instância que originou o evento.
timestamp string (RFC3339) Quando o Catcher registrou o evento (não necessariamente quando o WhatsApp gerou).
data object Payload especifico do evento. Shape depende de type — veja os blocos abaixo.

Troubleshooting — se o seu consumer responde 400 Bad Request: "missing event fields" ou "malformed envelope", o parser do receiver está esperando uma chave de envelope com nome diferente do que o Catcher envia (tipicamente event em vez de type). A correção e no consumer: leia body.type (não body.event). Cheque também se o consumer usa express.raw() (ou equivalente) para preservar os bytes exatos do body — JSON.parse + JSON.stringify muda a ordem das chaves e quebra a verificação HMAC (veja seção anterior).

Campos de mutualidade (is_mutual / mutuality_checked_at) — todos os eventos que carregam um phone também podem trazer is_mutual (bool) e mutuality_checked_at (string ISO-8601). is_mutual=true significa que o contato resolvido tem a instância salva na agenda dele (lido do cache contact_mutuality_cache, sem roundtrip extra com o Meta). Ambos são omitidos quando a resposta não está em cache — permite distinguir "não-mutuo" de "desconhecido".

message.received#

Disparado quando uma mensagem e recebida no WhatsApp. Persistido na tabela messages. Para mensagens de mídia (image, video, audio, document, sticker), o arquivo e baixado do WhatsApp, armazenado no S3 e um registro Media e criado. O campo media_id pode ser usado para baixar a mídia via GET /v1/instances/{instanceId}/media/{mediaId}. Mensagens de status@broadcast e ecos da própria instância (IsFromMe) não geram esse evento.

json
{
  "phone": "554192464230",
  "from": "554192464230@s.whatsapp.net",
  "chat": "554192464230@s.whatsapp.net",
  "sender_lid": "216251804708983:34@lid",
  "chat_lid": "216251804708983@lid",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440000"],
  "type": "audio",
  "content": "",
  "media_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "mime_type": "audio/ogg; codecs=opus",
  "file_size": 34567,
  "duration": 15,
  "quoted_msg_id": "d28b6749-0a5c-47bc-9c12-fd4537a5d149",
  "timestamp": 1741360245,
  "is_group": false,
  "push_name": "Joao Silva"
}

Exemplo de mensagem de grupo:

json
{
  "phone": "554199999999",
  "from": "554199999999@s.whatsapp.net",
  "chat": "120363025555555555@g.us",
  "message_ids": ["76089f45-b309-43eb-9f2b-198ce302aa28"],
  "type": "text",
  "content": "oi pessoal",
  "timestamp": 1741360245,
  "is_group": true,
  "push_name": "Joao Silva",
  "group_name": "Vendas 2026",
  "group_subject": "Time comercial"
}

Para grupos: chat e o JID do grupo (@g.us), from/phone/push_name identificam o participante que enviou, e group_name/group_subject descrevem o grupo. Veja Mensagens de Grupo para o padrão completo.

Exemplo com contexto de anuncio (Instagram/Facebook click-to-WhatsApp):

json
{
  "phone": "554188322497",
  "from": "554188322497@s.whatsapp.net",
  "chat": "554188322497@s.whatsapp.net",
  "message_ids": ["ACDFD74C4368A916E0FE"],
  "type": "text",
  "content": "Ola! Tenho interesse no tratamento com o iModel",
  "ad_context": {
    "source_type": "instagram",
    "source_app": "Instagram",
    "source_id": "ad-123",
    "title": "Tratamento iModel",
    "body": "Agende sua consulta agora",
    "media_type": "image",
    "ctwa_clid": "ARA...",
    "ref": "utm_campaign_xyz",
    "thumbnail_url": "https://media.catcher.one/biazap_abc/ads/thumbs/7a3b1c...d.jpg",
    "media_url": "https://media.catcher.one/biazap_abc/ads/media/91ef02...c.mp4"
  },
  "is_forwarded": false,
  "timestamp": 1741360245,
  "is_group": false,
  "push_name": "Patricia"
}

Exemplo de contato compartilhado (type=contact):

json
{
  "phone": "554196332719",
  "from": "554196332719@s.whatsapp.net",
  "chat": "554196332719@s.whatsapp.net",
  "message_ids": ["b1e2c3d4-5678-90ab-cdef-1234567890ab"],
  "type": "contact",
  "content": "Amor",
  "contacts": [
    {
      "display_name": "Amor",
      "phones": ["5541988887777"],
      "vcard": "BEGIN:VCARD\nVERSION:3.0\nN:;Amor;;;\nFN:Amor\nTEL;type=CELL;type=VOICE;waid=5541988887777:+55 41 8888-7777\nEND:VCARD"
    }
  ],
  "timestamp": 1741360245,
  "is_group": false,
  "push_name": "Adriano"
}

Compartilhar varios contatos de uma vez (ou enviar via POST .../messages/contact com contacts: [...]) produz um ContactsArrayMessage: o mesmo evento type=contact com uma entrada em contacts por contato.

Campo Tipo Descricao
phone string Número de telefone sem sufixo (ex: "554192464230"). Resolvido do registro de identidade quando o sender e um LID
from string JID do remetente (resolvido para phone JID quando possível)
chat string JID do chat (igual a from em DM, JID do grupo em grupo; resolvido para phone JID quando possível)
sender_lid string LID original do remetente, presente apenas quando o WhatsApp usou LID internamente. Util para correlação (ver seção 22)
chat_lid string LID original do chat, presente apenas quando o WhatsApp usou LID internamente
message_ids []string Array com 1 UUID interno do Catcher (v4) — leia message_ids[0]. Identificador único e estável, nunca reciclado.
type string Tipo: text, image, video, audio, document, sticker, location, contact, reaction, poll, live_location, list, group_invite, view_once
content string Texto, caption, ou emoji (para reactions). Para type=contact, e o nome de exibição do(s) contato(s) — use contacts para os dados estruturados
contacts []object Cartoes de contato compartilhados. Presente apenas quando type=contact. Um contato gera 1 entrada; varios contatos de uma vez geram 1 entrada por contato. Ver campos de cada item abaixo
media_url string URL da mídia (deprecado, use media_id)
media_id string ID da mídia para download via API (presente se mídia foi armazenada com sucesso)
mime_type string MIME type da mídia (ex: audio/ogg; codecs=opus, image/jpeg)
file_name string Nome do arquivo (presente para documentos)
file_size uint64 Tamanho do arquivo em bytes (do proto WhatsApp)
duration uint32 Duração em segundos (para audio e video)
quoted_msg_id string UUID Catcher (v4) da mensagem sendo respondida. Presente apenas em respostas/reply threading. Vazio se a mensagem citada não estiver no tenant DB (ex: reply a mensagem antiga de antes da instância entrar no chat). Nunca e o hex do WhatsApp — use este campo para correlacionar com message_ids[0] de eventos anteriores.
reaction_target_id string UUID Catcher (v4) da mensagem que recebeu a reaction (apenas para type=reaction). Vazio se o alvo não estiver no tenant DB.
ad_context object Contexto de anuncio do Instagram/Facebook (presente apenas quando a mensagem originou de um click-to-WhatsApp ad). Ver campos abaixo
is_forwarded bool true se a mensagem foi encaminhada. Omitido quando false
timestamp int64 Unix timestamp
is_group bool Se a mensagem veio de um grupo
push_name string Nome do remetente no WhatsApp (em grupos, e o nome do participante que enviou)
group_name string Nome do grupo. Presente apenas quando is_group=true e o cache já foi populado (ver Mensagens de Grupo)
group_subject string Topico/descricao do grupo. Presente apenas quando is_group=true e o cache já foi populado
thread_message_id string UUID Catcher (v4) da mensagem cabeca-de-thread que este reply alveja. Vazio se a cabeca não estiver no tenant DB
verified_name string Nome comercial oficial registrado no Meta quando o remetente e um WhatsApp Business verificado. Vazio para contatos não-business
retry_count int Número de vezes que o dispositivo destino pediu reenvio (sync de sessão Signal). 0 no caminho feliz
width uint32 Largura em pixels (image, video, sticker). 0 caso contrario
height uint32 Altura em pixels (image, video, sticker). 0 caso contrario
page_count uint32 Total de páginas de um PDF/documento. 0 caso contrario
jpeg_thumbnail string (base64) Thumbnail inline pequena (4-8KB) para image/video/document, codificada em base64. Permite renderizar placeholder antes do download completo

Este evento também pode trazer is_mutual / mutuality_checked_at — ver a nota de mutualidade no início desta seção.

Campos de ad_context (todos opcionais, presentes conforme disponível no proto):

Campo Tipo Descricao
source_type string Plataforma de origem (ex: "instagram", "facebook")
source_app string Nome do app de origem (ex: "Instagram")
source_id string Identificador do anuncio na plataforma
title string Titulo/headline do anuncio
body string Texto descritivo do anuncio
media_type string Tipo de mídia do anuncio: "image", "video", ou vazio
ctwa_clid string Click-to-WhatsApp Client ID (tracking)
ref string Parametro de referência/tracking (ex: UTM campaign)
thumbnail_url string URL Catcher R2 da thumbnail do anuncio. O worker baixa a imagem via proxy da instância e persiste em {company}/ads/thumbs/{sha256}.{ext} no R2, retornando a URL pública. Quando o proto traz a thumbnail inline (campo Thumbnail []byte), o upload e direto — sem HTTP. Campo vazio se o cache falhou ou a instância não tem proxy bindado. Nunca será URL Meta/CDN.
media_url string URL Catcher R2 da mídia completa do anuncio (imagem/video grande). Worker baixa via proxy da instância e persiste em {company}/ads/media/{sha256}.{ext}. Campo vazio se cache falhou ou proxy indisponível. Nunca será URL Meta/CDN. Video: cap de 30 MB; excedeu, skip.

Proxy-discipline (licoes #29, #31, #35): As URLs acima (thumbnail_url, media_url) são sempre URLs Catcher — seja URL pública do R2 (quando S3_PUBLIC_URL está configurado) ou vazio. O worker NUNCA expoe URLs brutas do Meta (cdninstagram.com, fbcdn.net, scontent.*, facebook.com/*/videos/*) ao consumer. Todas as imagens/videos de anuncios são baixados pelo proxy dedicado da instância e republicados via R2 com dedup por sha256 do conteúdo (mesma thumbnail em 100 instâncias da mesma empresa = 1 objeto no R2). O campo source_url do proto Meta (permalink do post Instagram/Facebook) permanece intencionalmente removido — consumidores podem reconstruir referência via source_id + source_type quando necessário. Cache e best-effort: uma falha de rede ou ausência de binding emite campo vazio, sem quebrar o webhook.

Campos de cada item de contacts (presente apenas quando type=contact):

Campo Tipo Descricao
display_name string Nome de exibição do contato (proto DisplayName; cai no FN do vCard quando vazio)
phones []string Números de telefone extraidos do vCard, so digitos (ex: "5541988887777"). O parametro waid= (número normalizado do WhatsApp) tem prioridade sobre o valor formatado do campo TEL. Vazio se o vCard não tem linha TEL
vcard string vCard 3.0 cru, exatamente como veio no proto. Fidelidade total (email, organização, multiplos números) para quem quiser parsear

Contatos: O campo top-level phone identifica quem enviou o cartao, não o contato compartilhado. Para os números do(s) contato(s) compartilhado(s), use contacts[].phones.

Reactions: Quando type=reaction, o campo content contem o emoji (ex: "❤️"). Um content vazio indica que a reaction foi removida. O campo reaction_target_id contem o ID da mensagem que recebeu a reaction.


message.sent#

Disparado após envio bem-sucedido de qualquer mensagem via API. Persistido na tabela messages. Para mensagens de mídia, inclui media_id para download via GET /v1/instances/{instanceId}/media/{mediaId}.

json
{
  "phone": "554192464230",
  "to": "554192464230@s.whatsapp.net",
  "chat": "554192464230@s.whatsapp.net",
  "chat_lid": "42812401807390@lid",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440001"],
  "type": "image",
  "content": "Foto do produto",
  "media_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "mime_type": "image/jpeg",
  "file_name": "produto.jpg",
  "file_size": 123456,
  "source": "api",
  "idempotency_key": "msg-2026-03-21-0001",
  "quoted_msg_id": "3EB0AAA111BBB222",
  "timestamp": 1741360300,
  "is_group": false
}

Exemplo com varios contatos (type=contact):

json
{
  "phone": "554137984905",
  "to": "554137984905@s.whatsapp.net",
  "chat": "554137984905@s.whatsapp.net",
  "message_ids": ["c2b4e6f0-1234-4ab5-9c0d-fedcba987654"],
  "type": "contact",
  "content": "Suporte Catcher, Comercial",
  "contacts": [
    {
      "display_name": "Suporte Catcher",
      "phones": ["5541988887777"],
      "vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Suporte Catcher\nTEL;type=CELL;type=VOICE;waid=5541988887777:+5541988887777\nEND:VCARD"
    },
    {
      "display_name": "Comercial",
      "phones": ["5541977776666"],
      "vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Comercial\nTEL;type=CELL;type=VOICE;waid=5541977776666:+5541977776666\nEND:VCARD"
    }
  ],
  "source": "api",
  "idempotency_key": "msg-2026-05-18-0042",
  "timestamp": 1741360500,
  "is_group": false
}

content em type=contact: com um único contato, e o display_name. Com varios, e a juncao dos display_name separados por ", " (ex: "Suporte Catcher, Comercial"). Para os dados estruturados, sempre use contacts[].

Campo Tipo Descricao
phone string Número de telefone sem sufixo (ex: "554192464230"). Para mensagens externas com LID, resolvido do registro de identidade; pode ser vazio no primeiro contato
to string JID do destinatário (resolvido para phone JID quando possível)
chat string JID do chat (igual a to em DM, JID do grupo em grupo; resolvido para phone JID quando possível)
chat_lid string LID original do chat, presente apenas quando o destinatário usava LID internamente. Util para correlação com receipts (ver seção 22)
message_ids []string Array com 1 UUID interno do Catcher (v4) — leia message_ids[0]. Identificador único e estável, nunca reciclado. message_ids[0] e o mesmo UUID retornado em message_id na resposta 202 do endpoint de envio — use este valor para correlacionar o webhook com a request original.
type string Tipo da mensagem (text, image, video, audio, document, sticker, location, contact, reaction, poll, template, forward)
content string Conteúdo/caption da mensagem, ou emoji (para reactions). Para type=contact, e o nome de exibição do(s) contato(s) — use contacts para os dados estruturados
contacts []object Cartoes de contato compartilhados. Presente apenas quando type=contact. Mesma estrutura de contacts em message.received (display_name, phones, vcard)
source string Origem do envio: "api" (enviado pela Catcher) ou "external" (enviado pelo WhatsApp Web, telefone ou outra API)
idempotency_key string Eco do header Idempotency-Key da request original. Presente apenas quando source="api". Util para casar o webhook com a request do cliente quando multiplas requests compartilham o mesmo destinatário no curto prazo. Ausente em outbound externo (WhatsApp Web/celular/outra API).
media_id string ID da mídia para download via API (presente para mensagens de mídia)
mime_type string MIME type da mídia
file_name string Nome do arquivo
file_size int64 Tamanho do arquivo em bytes
quoted_msg_id string UUID Catcher (v4) da mensagem sendo respondida. Presente apenas em respostas/reply threading. Vazio se a mensagem citada não estiver no tenant DB (ex: reply a mensagem antiga de antes da instância entrar no chat). Nunca e o hex do WhatsApp — use este campo para correlacionar com message_ids[0] de eventos anteriores.
reaction_target_id string UUID Catcher (v4) da mensagem que recebeu a reaction (apenas para type=reaction). Vazio se o alvo não estiver no tenant DB.
timestamp int64 Unix timestamp do envio
is_group bool Se a mensagem foi enviada para um grupo
group_name string Nome do grupo. Presente apenas quando is_group=true e o cache já foi populado (ver Mensagens de Grupo)
group_subject string Topico/descricao do grupo. Presente apenas quando is_group=true e o cache já foi populado
thread_message_id string UUID Catcher (v4) da mensagem cabeca-de-thread que este reply alveja. Vazio se a cabeca não estiver no tenant DB
retry_count int Número de vezes que a mensagem foi reconstruida antes do envio final. Diferente de 0 apenas em echo outbound de outro dispositivo que precisou retry. 0 no caminho feliz
width uint32 Largura em pixels (image, video, sticker). 0 caso contrario
height uint32 Altura em pixels (image, video, sticker). 0 caso contrario
page_count uint32 Total de páginas de um PDF/documento. 0 caso contrario
jpeg_thumbnail string (base64) Thumbnail inline pequena (4-8KB) para image/video/document, codificada em base64. Permite renderizar placeholder antes do download completo

Este evento também pode trazer is_mutual / mutuality_checked_at — ver a nota de mutualidade no início desta seção.

Captura de outbound externo: Mensagens enviadas por WhatsApp Web, pelo celular ou por outra API conectada ao mesmo número são capturadas automaticamente como message.sent com source: "external". Isso permite rastrear toda a comunicação outbound independente de onde foi originada. Mensagens enviadas pela própria Catcher via fila tem source: "api". A persistencia no banco também distingue: a coluna source na tabela messages armazena "api" ou "external". Em mensagens externas, quoted_msg_id e reaction_target_id também são preservados quando presentes, inclusive para message.sent de grupos (is_group=true).

LID em mensagens externas: Quando uma mensagem e enviada por outro dispositivo (WhatsApp Web, celular) para um contato cujo chat aparece como LID, a Catcher resolve o telefone usando 3 camadas de fallback:

  1. RecipientAlt (protocolo): o WhatsApp envia peer_recipient_pn no evento, contendo o telefone do destinatário — está e a fonte primaria e mais confiavel para outbound echo.
  2. MySQL (contact_identities): registro persistido de interacoes anteriores (SenderAlt, RecipientAlt, queries de contato).
  3. SQLite (whatsmeow_lid_map): cache do whatsmeow populado por syncs de grupo, mensagens de migração LID, e operações de envio.

Se o contato já foi visto em qualquer dessas fontes, phone, to e chat conterao o telefone resolvido. Caso contrario, phone ficara vazio e chat_lid contera o LID original — quando o contato responder (trazendo SenderAlt) ou aparecer em um sync de grupo, o mapeamento LID→telefone será aprendido automaticamente para futuras consultas.

Nota: Não existe API no protocolo WhatsApp para resolver LID→telefone sob demanda. GetUserInfo(lid) não retorna o telefone (so funciona na direcao PN→LID).


message.delivered#

Disparado quando o destinatário recebe a mensagem (dois checks cinza). Atualiza status=delivered e delivered_at na tabela messages.

json
{
  "phone": "554192464230",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440001", "550e8400-e29b-41d4-a716-446655440002"],
  "chat": "554192464230@s.whatsapp.net",
  "sender": "554192464230@s.whatsapp.net",
  "chat_lid": "42812401807390@lid",
  "sender_lid": "42812401807390:77@lid",
  "status": "delivered",
  "timestamp": 1741360310,
  "is_group": false
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (ex: "554192464230"). Resolvido via 5 niveis de fallback: JID direto → MySQL contact_identities por chat LID → MySQL por sender LID → SQLite whatsmeow_lid_map → busca na tabela messages
message_ids []string UUIDs internos do Catcher. Identificadores únicos e estáveis.
chat string JID do chat (resolvido para phone JID quando possível)
sender string JID de quem recebeu (resolvido para phone JID quando possível)
chat_lid string LID original do chat, presente quando o WhatsApp usou LID internamente
sender_lid string LID original do sender (com sufixo de dispositivo), presente quando o WhatsApp usou LID internamente
status string Sempre "delivered"
timestamp int64 Unix timestamp da entrega
is_group bool Se o chat e um grupo
group_name string Nome do grupo. Presente apenas quando is_group=true e o cache já foi populado (ver Mensagens de Grupo)
group_subject string Topico/descricao do grupo. Presente apenas quando is_group=true e o cache já foi populado

message.read#

Disparado quando o destinatário le a mensagem (dois checks azuis). Atualiza status=read e read_at na tabela messages.

json
{
  "phone": "554192464230",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440001"],
  "chat": "554192464230@s.whatsapp.net",
  "sender": "554192464230@s.whatsapp.net",
  "chat_lid": "42812401807390@lid",
  "sender_lid": "42812401807390:77@lid",
  "status": "read",
  "timestamp": 1741360350,
  "is_group": false
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo. Resolvido do registro de identidade quando o chat e um LID (mesma cadeia de fallback que message.delivered)
message_ids []string UUIDs internos do Catcher. Identificadores únicos e estáveis.
chat string JID do chat (resolvido para phone JID quando possível)
sender string JID de quem leu (resolvido para phone JID quando possível)
chat_lid string LID original do chat, presente quando o WhatsApp usou LID internamente
sender_lid string LID original do sender, presente quando o WhatsApp usou LID internamente
status string Sempre "read"
timestamp int64 Unix timestamp da leitura
is_group bool Se o chat e um grupo
group_name string Nome do grupo. Presente apenas quando is_group=true e o cache já foi populado (ver Mensagens de Grupo)
group_subject string Topico/descricao do grupo. Presente apenas quando is_group=true e o cache já foi populado

message.played#

Disparado quando o destinatário abre uma mídia "visualizar uma vez" (view-once). Atualiza status=played e played_at na tabela messages.

json
{
  "phone": "554192464230",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440001"],
  "chat": "554192464230@s.whatsapp.net",
  "sender": "554192464230@s.whatsapp.net",
  "chat_lid": "42812401807390@lid",
  "sender_lid": "42812401807390:77@lid",
  "status": "played",
  "timestamp": 1741360400,
  "is_group": false
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (mesma cadeia de fallback que message.delivered)
message_ids []string UUIDs internos do Catcher. Identificadores únicos e estáveis.
chat string JID do chat (resolvido para phone JID quando possível)
sender string JID de quem abriu (resolvido para phone JID quando possível)
chat_lid string LID original do chat, presente quando o WhatsApp usou LID internamente
sender_lid string LID original do sender, presente quando o WhatsApp usou LID internamente
status string Sempre "played"
timestamp int64 Unix timestamp da abertura
is_group bool Se o chat e um grupo
group_name string Nome do grupo. Presente apenas quando is_group=true e o cache já foi populado (ver Mensagens de Grupo)
group_subject string Topico/descricao do grupo. Presente apenas quando is_group=true e o cache já foi populado

message.deleted#

Disparado quando uma mensagem e apagada ("apagar para todos"). Atualiza status=deleted e deleted_at na tabela messages. Cobre ambas as direcoes: mensagens inbound apagadas pelo contato (is_from_me=false) e mensagens outbound apagadas pelo operador via WhatsApp phone/Web (is_from_me=true).

json
{
  "phone": "554192464230",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440000"],
  "chat": "554192464230@s.whatsapp.net",
  "sender": "554192464230@s.whatsapp.net",
  "chat_lid": "216251804708983@lid",
  "sender_lid": "216251804708983:34@lid",
  "is_from_me": false,
  "timestamp": 1741360400
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo
message_ids []string Array com 1 UUID interno do Catcher (v4) — leia message_ids[0]. Identificador único e estável, nunca reciclado.
chat string JID do chat (resolvido para phone JID quando possível)
sender string JID de quem apagou (resolvido para phone JID quando possível)
chat_lid string LID original do chat, presente quando o WhatsApp usou LID internamente
sender_lid string LID original do sender, presente quando o WhatsApp usou LID internamente
is_from_me bool Se foi a própria instância que apagou
is_group bool Se a mensagem apagada pertencia a um grupo
group_name string Nome do grupo. Presente apenas quando is_group=true e o cache já foi populado (ver Mensagens de Grupo)
group_subject string Topico/descricao do grupo. Presente apenas quando is_group=true e o cache já foi populado
timestamp int64 Unix timestamp

message.edited#

Disparado quando uma mensagem e editada. Atualiza content na tabela messages. Cobre ambas as direcoes: mensagens inbound editadas pelo contato (is_from_me=false) e mensagens outbound editadas pelo operador via WhatsApp phone/Web (is_from_me=true).

json
{
  "phone": "554192464230",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440000"],
  "chat": "554192464230@s.whatsapp.net",
  "sender": "554192464230@s.whatsapp.net",
  "chat_lid": "216251804708983@lid",
  "sender_lid": "216251804708983:34@lid",
  "new_content": "Texto editado da mensagem",
  "is_from_me": false,
  "is_group": false,
  "push_name": "Joao",
  "timestamp": 1741360450
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo
message_ids []string Array com 1 UUID interno do Catcher (v4) — leia message_ids[0]. Identificador único e estável, nunca reciclado.
chat string JID do chat (resolvido para phone JID quando possível)
sender string JID de quem editou (resolvido para phone JID quando possível)
chat_lid string LID original do chat, presente quando o WhatsApp usou LID internamente
sender_lid string LID original do sender, presente quando o WhatsApp usou LID internamente
new_content string Novo conteúdo da mensagem
is_from_me bool Se foi a própria instância que editou
is_group bool Se a mensagem e de um grupo
push_name string Nome do remetente (opcional)
group_name string Nome do grupo. Presente apenas quando is_group=true e o cache já foi populado (ver Mensagens de Grupo)
group_subject string Topico/descricao do grupo. Presente apenas quando is_group=true e o cache já foi populado
timestamp int64 Unix timestamp

call.received#

Disparado quando a instância recebe uma chamada. Persistido na tabela call_logs.

json
{
  "phone": "554192464230",
  "from": "554192464230@s.whatsapp.net",
  "call_id": "CALL-ABC123",
  "timestamp": 1741360500
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo
from string JID de quem ligou
call_id string ID único da chamada
timestamp int64 Unix timestamp

connection.update#

Disparado quando o status da conexão WhatsApp muda. Persistido na tabela instances.

json
{
  "status": "connected",
  "reason": "",
  "previous_status": "connecting",
  "ban_expiry": null
}
Campo Tipo Descricao
status string "connected", "disconnected", "banned", "logged_out", "replaced", "connect_failure", "client_outdated", "stream_error", "keepalive_timeout", "keepalive_restored"
reason string Classifica o gatilho. Para status="disconnected": flapping (detector de flap), qr_pending_cooldown (WS fechou esperando scan de QR), silent_close (TCP EOF/RST). Para banned/logged_out/replaced/connect_failure/etc, a string do evento original
previous_status string Status que a instância tinha imediatamente antes desta transicao (ex: distinguir QR_PENDING → DISCONNECTED de CONNECTED → DISCONNECTED). Omitido quando desconhecido
ban_expiry int64/null Unix timestamp de quando o ban expira (apenas para "banned")

Novos status:

  • connect_failure — falha ao conectar ao servidor WhatsApp (ex: erro de rede, autenticação falhou)
  • client_outdated — versão do cliente WhatsApp está desatualizada e precisa ser atualizada
  • stream_error — erro no stream de comunicação com o servidor WhatsApp
  • keepalive_timeout — keepalive não recebeu resposta a tempo; conexão pode estar instável
  • keepalive_restored — keepalive voltou a funcionar normalmente após um timeout

presence.update#

Disparado quando um contato fica online/offline ou está digitando. Evento transiente, não persistido no banco.

Nota: Para receber presença de digitacao, a instância precisa estar com presença available. Para presença online/offline, a Catcher so passa a monitorar após POST /v1/instances/{instanceId}/contacts/{contactId}/presence/subscribe.

Snapshot opcional: GET /v1/instances/{instanceId}/contacts/{contactId}/presence le o último estado conhecido em Redis (online, last_seen, typing_state). Isso não substitui SSE/WebSocket/webhook; e apenas um read model transiente para UX.

json
{
  "phone": "554192464230",
  "from": "554192464230@s.whatsapp.net",
  "available": true,
  "last_seen": 1741360000,
  "state": "composing",
  "is_chat": true,
  "chat": "554192464230@s.whatsapp.net",
  "is_group": false
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (vazio para grupos)
from string JID do contato
available bool Se está disponível/digitando
last_seen int64 Último visto (unix, 0 se oculto). Apenas para online/offline
state string "composing", "paused", "recording" (apenas para is_chat=true)
is_chat bool true = digitacao em chat, false = online/offline
chat string JID do chat onde está digitando (apenas para is_chat=true)
is_group bool Se e em um grupo

group.update#

Disparado para qualquer mudanca em grupo. Persistido na tabela group_events.

json
{
  "group_jid": "120363025555555555@g.us",
  "group_name": "Vendas 2026",
  "group_subject": "Time comercial",
  "action": "participant_join",
  "sender": "554192464230@s.whatsapp.net",
  "timestamp": 1741360600,
  "value": "",
  "members": ["554199999999@s.whatsapp.net", "554188888888@s.whatsapp.net"]
}
Campo Tipo Descricao
group_jid string JID do grupo
group_name string Nome atual do grupo (cache). Em name_change reflete o novo nome já atualizado. Pode estar vazio se for a primeira vez que esse grupo aparece em um evento (ver Mensagens de Grupo)
group_subject string Topico/descricao atual do grupo (cache). Em topic_change reflete o novo topico
action string Tipo da mudanca (veja tabela abaixo)
sender string JID de quem fez a mudanca
sender_pn string JID de telefone equivalente quando sender e um LID (de events.GroupInfo.SenderPN). Vazio quando sender já e telefone ou o Meta não forneceu o par
join_reason string "invite" quando o participant_join veio de link de convite (em vez de admin-add). Vazio para admin-add e ações não-join
timestamp int64 Unix timestamp
value string Valor novo (nome, topico, picture_id)
members []string JIDs dos membros afetados

Ações possíveis:

Action Descricao value members
name_change Nome do grupo alterado Novo nome -
topic_change Descricao alterada Nova descricao -
participant_join Membros entraram - JIDs que entraram
participant_leave Membros sairam - JIDs que sairam
participant_promote Promovidos a admin - JIDs promovidos
participant_demote Rebaixados de admin - JIDs rebaixados
locked Apenas admins editam info - -
unlocked Todos editam info - -
announce Apenas admins enviam msgs - -
unannounce Todos enviam msgs - -
picture_change Foto do grupo alterada Picture ID -
picture_remove Foto do grupo removida - -
ephemeral_change Mensagens temporarias ativadas/desativadas Duração em segundos (0 = desativado) -
membership_approval_on Aprovacao de membros ativada - -
membership_approval_off Aprovacao de membros desativada - -
group_deleted Grupo foi excluido - -
group_linked Grupo foi vinculado a uma comunidade JID da comunidade -
group_unlinked Grupo foi desvinculado de uma comunidade JID da comunidade -
invite_link_reset Link de convite foi redefinido Novo link -
suspended Grupo foi suspenso pelo WhatsApp - -
unsuspended Grupo foi reativado pelo WhatsApp - -

<a id="mensagens-de-grupo"></a>

Mensagens de Grupo (semantica + cache)

Quando uma mensagem chega de um grupo, os campos do payload tem a seguinte semantica:

Campo DM (1:1) Grupo
chat JID do contato (@s.whatsapp.net) JID do grupo (@g.us)
from JID do contato JID do participante que enviou (resolvido para phone JID quando possível)
phone Telefone do contato Telefone do participante que enviou
push_name Nome do contato Nome do participante que enviou
is_group false true
group_name omitido Nome do grupo (cache, ver abaixo)
group_subject omitido Topico/descricao do grupo (cache, ver abaixo)

Padrão de renderizacao: Para mostrar uma mensagem de grupo no estilo WhatsApp Web, use group_name como titulo da conversa e push_name como autor do balao individual. chat e o identificador único da conversa de grupo (sempre termina em @g.us).

Cache de metadados de grupo (lazy):

group_name e group_subject são populados a partir de um cache em memória por instância. O cache funciona assim:

  1. Primeira mensagem de um grupo novo: o cache está vazio. O evento e emitido com group_name="" e group_subject="". Em paralelo, a Catcher dispara uma busca assincrona via GetGroupInfo para popular o cache.
  2. Mensagens subsequentes: o cache já tem o nome, entao todos os eventos do mesmo grupo virao com group_name e group_subject preenchidos.
  3. Eventos events.GroupInfo com name_change ou topic_change atualizam o cache imediatamente, antes mesmo de emitir o group.update correspondente.
  4. Logout/delete da instância limpa o cache para evitar leakage entre sessões.

Quais eventos incluem group_name / group_subject?

Evento group_name group_subject
message.received Sim Sim
message.sent Sim Sim
message.delivered Sim Sim
message.read Sim Sim
message.played Sim Sim
message.deleted Sim Sim
message.edited Sim Sim
group.update Sim Sim
group.joined Sim (group_name/group_topic já existiam) Sim

Fallback para grupos novos: Se o seu consumidor recebe um evento de grupo com group_name vazio, você pode chamar GET /v1/instances/{instanceId}/groups/{groupJid} para resolver o nome no momento (e isso já vai popular o cache para os próximos eventos).


contact.update#

Disparado quando a foto de perfil, nome ou status de um contato muda. Persistido na tabela contact_events.

Quando o contato e identificado por LID (Linked Device ID), o campo jid mostra o JID do telefone (se resolvido) e jid_lid preserva o LID original. Se o LID não puder ser resolvido, jid contera o LID e phone ficara vazio.

json
{
  "phone": "554192464230",
  "jid": "554192464230@s.whatsapp.net",
  "jid_lid": "273293080838230@lid",
  "action": "picture_change",
  "value": "j+rE",
  "timestamp": 1741360700
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (vazio se LID não resolvido ou grupo)
jid string JID do contato (telefone resolvido quando possível)
jid_lid string LID original quando o JID foi resolvido de um LID (vazio se já era telefone)
action string Tipo da mudanca (veja tabela abaixo)
value string Valor novo (ID/hash da foto, novo nome, etc.)
old_value string Valor anterior (presente apenas para push_name_change e business_name_change)
timestamp int64 Unix timestamp

Ações possíveis:

Action Descricao value old_value
picture_change Foto de perfil alterada Picture ID ou hash -
picture_remove Foto de perfil removida - -
about_change Status/about alterado Novo status -
push_name_change Nome de exibição alterado Novo nome Nome anterior
business_name_change Nome comercial alterado Novo nome Nome anterior

Avatar cache: Quando action e picture_change, a Catcher automaticamente baixa a nova foto do contato e armazena no S3/R2. Quando action e picture_remove, o cache e limpo. Clientes podem usar este evento para invalidar avatares em cache local e buscar a nova versão via GET /v1/instances/{instanceId}/contacts/{phone}/avatar.


instance.banned#

Disparado quando a instância recebe um ban temporario ou permanente do WhatsApp. Persistido nos campos ban_expiry e ban_reason da instância. Diferente de connection.update com status: "banned", este evento traz informações detalhadas do ban.

json
{
  "instance_id": "84c2e480-...",
  "permanent": false,
  "reason": "spam",
  "ban_expiry": 1741446000
}
Campo Tipo Descricao
instance_id string ID da instância banida
permanent bool true = ban permanente, false = temporario
reason string Motivo do ban (quando disponível)
ban_expiry int64/null Unix timestamp de quando o ban expira (null se permanente)

Nota: O evento connection.update com status: "banned" também e emitido. Use instance.banned quando precisar de detalhes do ban (permanencia, motivo, expiracao).


instance.offline / instance.critical_offline / instance.recovered#

Eventos do monitor de saúde (internal/health/monitor.go). Disparados quando uma instância cruza um dos thresholds de tempo offline:

  • instance.offline — instância desconectada ha 15min ou mais (transicao única do tier degraded para offline).
  • instance.critical_offline — instância desconectada ha 6h ou mais (transicao do tier offline para critical_offline). Também dispara email para o owner da empresa + todos os superadmins.
  • instance.recovered — instância voltou a conectar após ter ficado em offline/critical_offline (limpa o offline_alert_level).

Cada evento e disparado exatamente uma vez por transicao. O monitor armazena o nivel atual em offline_alert_level na linha da instância para evitar re-emissoes a cada scan (60s). Instâncias com desired_state=DISCONNECTED (pausadas manualmente) nunca disparam estes eventos.

json
{
  "instance_id": "84c2e480-...",
  "name": "EJ Whats",
  "phone": "554192464230",
  "tier": "critical_offline",
  "previous_tier": "offline",
  "status": "DISCONNECTED",
  "desired_state": "CONNECTED",
  "disconnected_at": "2026-04-06T21:44:00Z",
  "last_activity_at": "2026-04-06T21:42:18Z",
  "offline_duration_seconds": 158400,
  "offline_duration_human": "1d 20h",
  "reason": "",
  "timestamp": 1744156800
}
Campo Tipo Descricao
instance_id string ID da instância
name string Nome configurado da instância
phone string Telefone E.164 da instância (vazio se nunca conectou)
tier string Tier no momento da emissao: offline, critical_offline, healthy, stale, etc.
previous_tier string Tier antes da transicao (util para correlacionar)
status string Status WhatsApp bruto: CONNECTED, DISCONNECTED, etc.
desired_state string CONNECTED (queremos restaurar) ou DISCONNECTED (pausada)
disconnected_at string/null Timestamp ISO-8601 da primeira deteccao do drop. Preserva o tempo original em flapping.
last_activity_at string/null Último heartbeat (mensagem inbound, send outbound, ou receipt processado)
offline_duration_seconds int64 Duração calculada no momento da emissao
offline_duration_human string Versão formatada ("2d 4h", "30m") para display direto
reason string Conteúdo de last_error se houver
timestamp int64 Unix seconds da emissao

Configuração: Os thresholds (15min e 6h) são constantes no código (internal/health/tier.go) para garantir visibilidade em code review. Para tunar, edite OfflineThreshold e CriticalThreshold.

Endpoint relacionado: GET /v1/admin/instance-health retorna o rollup completo cross-tenant em tempo real (usado pelo banner do dashboard, badge da sidebar, e card "Saúde das instâncias").


message.undecryptable#

Disparado quando uma mensagem e recebida mas não pode ser decriptada pela instância. Isso pode acontecer quando a sessão de criptografia está dessincronizada ou quando a mensagem foi enviada para um dispositivo anterior. Persistido na tabela messages com status undecryptable.

json
{
  "phone": "554192464230",
  "from": "554192464230@s.whatsapp.net",
  "chat": "554192464230@s.whatsapp.net",
  "message_ids": ["3EB0ABC123DEF456"],
  "is_unavailable": true,
  "unavailable_type": "account_removed",
  "decrypt_fail_mode": "",
  "timestamp": 1741360800
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo
from string JID do remetente
chat string JID do chat
message_ids []string Array com 1 elemento (message_ids[0]) contendo o ID hexadecimal do WhatsApp. Exceção: message.undecryptable e o único evento que carrega o ID do WhatsApp e não um UUID Catcher, porque a mensagem não pode ser persistida.
is_unavailable bool true se a mensagem foi marcada como indisponível pelo servidor
unavailable_type string Tipo de indisponibilidade (ex: "account_removed", vazio se não aplicavel)
decrypt_fail_mode string Modo de falha da decriptacao (ex: "no_session", vazio se is_unavailable=true)
timestamp int64 Unix timestamp

message.starred#

Disparado quando uma mensagem e marcada ou desmarcada com estrela. Evento de sincronização entre dispositivos; não persistido em tabela separada.

json
{
  "chat": "554192464230@s.whatsapp.net",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440000"],
  "starred": true,
  "is_from_me": false,
  "timestamp": 1741360850
}
Campo Tipo Descricao
chat string JID do chat
message_ids []string Array com 1 UUID interno do Catcher (v4) — leia message_ids[0]. Identificador único e estável, nunca reciclado.
starred bool true = marcada com estrela, false = desmarcada
is_from_me bool Se a mensagem e da própria instância
timestamp int64 Unix timestamp

message.deleted_for_me#

Disparado quando uma mensagem e apagada localmente ("apagar para mim"), sem afetar os outros participantes. Diferente de message.deleted, que e "apagar para todos". Não persistido.

json
{
  "chat": "554192464230@s.whatsapp.net",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440000"],
  "is_from_me": false,
  "timestamp": 1741360900
}
Campo Tipo Descricao
chat string JID do chat
message_ids []string Array com 1 UUID interno do Catcher (v4) — leia message_ids[0]. Identificador único e estável, nunca reciclado.
is_from_me bool Se a mensagem era da própria instância
timestamp int64 Unix timestamp

call.accepted#

Disparado quando uma chamada feita pela instância e aceita pela parte remota. Persistido na tabela call_logs.

json
{
  "phone": "554192464230",
  "from": "554192464230@s.whatsapp.net",
  "call_id": "CALL-ABC123",
  "timestamp": 1741360950
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo
from string JID de quem aceitou
call_id string ID único da chamada
timestamp int64 Unix timestamp

call.rejected#

Disparado quando uma chamada e rejeitada pela parte remota. Persistido na tabela call_logs.

json
{
  "phone": "554192464230",
  "from": "554192464230@s.whatsapp.net",
  "call_id": "CALL-ABC123",
  "timestamp": 1741361000
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo
from string JID de quem rejeitou
call_id string ID único da chamada
timestamp int64 Unix timestamp

call.ended#

Disparado quando uma chamada e finalizada por qualquer motivo. Persistido na tabela call_logs.

json
{
  "phone": "554192464230",
  "from": "554192464230@s.whatsapp.net",
  "call_id": "CALL-ABC123",
  "reason": "normal",
  "duration": 45,
  "timestamp": 1741361050
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo
from string JID da outra parte
call_id string ID único da chamada
reason string Motivo do encerramento (ex: "normal", "timeout", "busy", "unavailable")
duration int Duração da chamada em segundos (0 se não atendida)
timestamp int64 Unix timestamp

call.group_offer#

Disparado quando a instância recebe uma chamada de grupo. Persistido na tabela call_logs.

json
{
  "call_id": "CALL-GRP456",
  "media": "audio",
  "type": "group",
  "timestamp": 1741361100
}
Campo Tipo Descricao
call_id string ID único da chamada
media string Tipo de mídia: "audio" ou "video"
type string Sempre "group"
timestamp int64 Unix timestamp

group.joined#

Disparado quando a instância e adicionada a um grupo (por convite, link ou admin). Persistido na tabela group_events.

json
{
  "group_jid": "120363025555555555@g.us",
  "group_name": "Vendas 2026",
  "group_topic": "Canal de vendas da equipe",
  "reason": "invite",
  "sender": "554192464230@s.whatsapp.net",
  "timestamp": 1741361150
}
Campo Tipo Descricao
group_jid string JID do grupo
group_name string Nome do grupo
group_topic string Descricao/topico do grupo
reason string Motivo da entrada (ex: "invite", "link", "admin_add")
sender string JID de quem adicionou (quando disponível)
sender_pn string JID de telefone equivalente de sender quando ele e um LID. Vazio quando sender já e telefone ou o Meta não forneceu o par
timestamp int64 Unix timestamp

blocklist.update#

Disparado quando a lista de contatos bloqueados e alterada. Não persistido.

json
{
  "action": "modify",
  "changes": [
    {
      "phone": "554192464230",
      "jid": "554192464230@s.whatsapp.net",
      "action": "block"
    },
    {
      "phone": "554188322497",
      "jid": "554188322497@s.whatsapp.net",
      "jid_lid": "216251804708983@lid",
      "action": "unblock"
    }
  ],
  "timestamp": 1741361200
}
Campo Tipo Descricao
action string "set" (lista completa substituida) ou "modify" (alteração incremental)
changes array Lista de alterações
changes[].phone string Número de telefone sem sufixo
changes[].jid string JID do contato (telefone resolvido quando possível)
changes[].jid_lid string LID original quando resolvido de um LID
changes[].action string "block" ou "unblock"
timestamp int64 Unix timestamp

Nota: Quando action e "set", a lista changes representa a lista completa de contatos bloqueados (todos com action: "block"). Quando e "modify", cada item em changes pode ser "block" ou "unblock".


contact.identity_changed#

Disparado quando um contato troca de dispositivo principal (re-registrou o WhatsApp em outro celular). A chave de criptografia do contato mudou. Persistido na tabela contact_events.

json
{
  "phone": "554192464230",
  "jid": "554192464230@s.whatsapp.net",
  "jid_lid": "273293080838230@lid",
  "implicit": false,
  "timestamp": 1741361250
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo
jid string JID do contato (telefone resolvido quando possível)
jid_lid string LID original quando resolvido de um LID
implicit bool true se a mudanca foi detectada implicitamente (sem notificação do servidor), false se o servidor notificou explicitamente
timestamp int64 Unix timestamp

contact.sync#

Disparado quando um contato e sincronizado do dispositivo (agenda de contatos). Persistido na tabela contact_events.

json
{
  "phone": "554192464230",
  "jid": "554192464230@s.whatsapp.net",
  "first_name": "Joao",
  "full_name": "Joao Silva",
  "timestamp": 1741361300
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo
jid string JID do contato
first_name string Primeiro nome do contato (da agenda)
full_name string Nome completo do contato (da agenda)
timestamp int64 Unix timestamp

chat.pin#

Disparado quando um chat e fixado ou desfixado. Evento de sincronização entre dispositivos. Não persistido.

json
{
  "phone": "554192464230",
  "chat": "554192464230@s.whatsapp.net",
  "chat_lid": "216251804708983@lid",
  "pinned": true,
  "timestamp": 1741361350
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (vazio para grupos ou LID não resolvido)
chat string JID do chat (telefone resolvido quando possível)
chat_lid string LID original quando o chat foi resolvido de um LID
pinned bool true = fixado, false = desfixado
timestamp int64 Unix timestamp

chat.archive#

Disparado quando um chat e arquivado ou desarquivado. Evento de sincronização entre dispositivos. Não persistido.

json
{
  "phone": "554192464230",
  "chat": "554192464230@s.whatsapp.net",
  "chat_lid": "216251804708983@lid",
  "archived": true,
  "timestamp": 1741361400
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (vazio para grupos ou LID não resolvido)
chat string JID do chat (telefone resolvido quando possível)
chat_lid string LID original quando o chat foi resolvido de um LID
archived bool true = arquivado, false = desarquivado
timestamp int64 Unix timestamp

chat.mute#

Disparado quando um chat e silenciado ou dessilenciado. Evento de sincronização entre dispositivos. Não persistido.

json
{
  "phone": "554192464230",
  "chat": "554192464230@s.whatsapp.net",
  "chat_lid": "216251804708983@lid",
  "muted": true,
  "mute_end": 1741447800,
  "timestamp": 1741361450
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (vazio para grupos ou LID não resolvido)
chat string JID do chat (telefone resolvido quando possível)
chat_lid string LID original quando o chat foi resolvido de um LID
muted bool true = silenciado, false = dessilenciado
mute_end int64 Unix timestamp de quando o silenciamento expira (0 se silenciado indefinidamente ou se muted=false)
timestamp int64 Unix timestamp

chat.read_state#

Disparado quando um chat e marcado como lido ou não lido. Emitido tanto por sincronização entre dispositivos (AppState) quanto pelas APIs POST /v1/instances/{id}/mark-read (com mark_all: true) e POST /v1/instances/{id}/mark-unread. Não persistido.

json
{
  "phone": "554192464230",
  "chat": "554192464230@s.whatsapp.net",
  "chat_lid": "216251804708983@lid",
  "marked_as_read": false,
  "timestamp": 1741361500
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (vazio para grupos ou LID não resolvido)
chat string JID do chat (telefone resolvido quando possível)
chat_lid string LID original quando o chat foi resolvido de um LID
marked_as_read bool true = marcado como lido, false = marcado como não lido
timestamp int64 Unix timestamp

Nota: Quando disparado via API (mark-read / mark-unread), o evento e emitido imediatamente após o SendAppState bem-sucedido, sem esperar o echo de sincronização do WhatsApp.


chat.clear#

Disparado quando um chat e limpo (histórico apagado). Não persistido.

json
{
  "phone": "554192464230",
  "chat": "554192464230@s.whatsapp.net",
  "chat_lid": "216251804708983@lid",
  "delete_media": false,
  "timestamp": 1741361550
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (vazio para grupos ou LID não resolvido)
chat string JID do chat (telefone resolvido quando possível)
chat_lid string LID original quando o chat foi resolvido de um LID
delete_media bool true se a mídia também foi apagada
timestamp int64 Unix timestamp

chat.delete#

Disparado quando um chat e apagado completamente. Não persistido.

json
{
  "phone": "554192464230",
  "chat": "554192464230@s.whatsapp.net",
  "chat_lid": "216251804708983@lid",
  "delete_media": true,
  "timestamp": 1741361600
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (vazio para grupos ou LID não resolvido)
chat string JID do chat (telefone resolvido quando possível)
chat_lid string LID original quando o chat foi resolvido de um LID
delete_media bool true se a mídia também foi apagada
timestamp int64 Unix timestamp

privacy.update#

Disparado quando as configurações de privacidade da instância são alteradas. Não persistido.

json
{
  "settings": {
    "group_add": "contacts",
    "last_seen": "everyone",
    "online": "match_last_seen",
    "profile_photo": "contacts",
    "status": "contacts",
    "read_receipts": "enabled",
    "call_add": "known"
  },
  "changed_fields": ["group_add", "profile_photo"],
  "timestamp": 1741361650
}
Campo Tipo Descricao
settings object Mapa de configurações de privacidade atuais (nome_config -> valor)
changed_fields []string Lista dos campos que foram alterados nesta atualização
timestamp int64 Unix timestamp

Configurações possíveis: group_add, last_seen, online, profile_photo, status, read_receipts, call_add. Os valores dependem da configuração (ex: "everyone", "contacts", "nobody", "match_last_seen", "enabled", "disabled", "known").


history.sync#

Disparado quando uma sincronização de histórico e recebida do servidor WhatsApp (tipicamente após conectar um novo dispositivo ou após POST /history/import). As mensagens do blob são persistidas no histórico de conversas; o evento abaixo e o resumo da importação.

json
{
  "sync_type": "RECENT",
  "conversation_count": 42,
  "message_count": 1500,
  "pushname_count": 38,
  "imported_count": 1480,
  "duplicate_count": 20,
  "skipped_count": 0,
  "failed_count": 0,
  "timestamp": 1741361700
}
Campo Tipo Descricao
sync_type string Tipo de sincronização (ex: "RECENT", "FULL", "PUSH_NAME")
conversation_count int Número de conversas sincronizadas
message_count int Número de mensagens sincronizadas
pushname_count int Número de push names sincronizados
imported_count int Mensagens históricas gravadas em messages
duplicate_count int Mensagens já existentes ignoradas por dedupe
skipped_count int Itens sem mensagem/chave válida ou ignorados
failed_count int Itens que falharam ao converter ou persistir
timestamp int64 Unix timestamp

Histórico importado não emite message.received/message.sent por mensagem, para evitar que consumidores processem mensagens antigas como tráfego novo. Use history.sync para progresso/resumo e GET /chats/{chatId}/messages para ler o conteúdo salvo.


sync.preview#

Disparado quando um preview de sincronização offline e recebido, indicando quantos eventos estão pendentes. Não persistido.

json
{
  "total": 250,
  "messages": 180,
  "receipts": 45,
  "notifications": 25,
  "timestamp": 1741361750
}
Campo Tipo Descricao
total int Total de eventos pendentes
messages int Número de mensagens pendentes
receipts int Número de receipts (entrega/leitura) pendentes
notifications int Número de notificações pendentes
timestamp int64 Unix timestamp

média.retry_result#

Disparado quando o resultado de um retry de download de mídia e recebido. Quando a mídia de uma mensagem recebida não pode ser baixada inicialmente, o WhatsApp pode reenviar os bytes — este evento indica o resultado dessa tentativa. Não persistido.

json
{
  "message_ids": ["3EB0ABC123DEF456"],
  "chat": "554192464230@s.whatsapp.net",
  "success": true,
  "timestamp": 1741361800
}
Campo Tipo Descricao
message_ids []string Array com 1 elemento (message_ids[0]) contendo o UUID Catcher da mensagem cuja mídia foi retentada
chat string JID do chat
success bool true se o retry foi bem-sucedido
error_code int Código de erro (presente apenas quando success=false)
timestamp int64 Unix timestamp

label.edit#

Disparado quando uma etiqueta e criada, editada ou removida. Etiquetas são um recurso do WhatsApp Business. Não persistido.

json
{
  "label_id": "5",
  "name": "Urgente",
  "color": 1,
  "deleted": false,
  "timestamp": 1741361850
}
Campo Tipo Descricao
label_id string ID da etiqueta
name string Nome da etiqueta
color int Índice de cor da etiqueta (0-19 no WhatsApp Business)
deleted bool true se a etiqueta foi removida
timestamp int64 Unix timestamp

label.chat_association#

Disparado quando um chat e associado ou desassociado de uma etiqueta. Não persistido.

json
{
  "label_id": "5",
  "chat": "554192464230@s.whatsapp.net",
  "phone": "554192464230",
  "associated": true,
  "timestamp": 1741361900
}
Campo Tipo Descricao
label_id string ID da etiqueta
chat string JID do chat
phone string Número de telefone sem sufixo (vazio para grupos)
associated bool true = chat etiquetado, false = chat desetiquetado
timestamp int64 Unix timestamp

label.message_association#

Disparado quando uma mensagem e associada ou desassociada de uma etiqueta. Não persistido.

json
{
  "label_id": "5",
  "chat": "554192464230@s.whatsapp.net",
  "message_ids": ["3EB0ABC123DEF456"],
  "associated": true,
  "timestamp": 1741361950
}
Campo Tipo Descricao
label_id string ID da etiqueta
chat string JID do chat
message_ids []string Array com 1 elemento (message_ids[0]) contendo o UUID Catcher da mensagem etiquetada
associated bool true = mensagem etiquetada, false = mensagem desetiquetada
timestamp int64 Unix timestamp

message.acked#

Disparado quando outro dispositivo seu (sync multi-device) confirma recebimento de uma mensagem que você enviou. Distinto de message.delivered — aquele e o contato confirmando entrega; este e o seu próprio segundo aparelho. Não persistido (não carimba delivered_at na tabela messages).

json
{
  "phone": "554192464230",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440001"],
  "chat": "554192464230@s.whatsapp.net",
  "sender": "554192464230@s.whatsapp.net",
  "chat_lid": "42812401807390@lid",
  "sender_lid": "42812401807390:77@lid",
  "status": "acked",
  "timestamp": 1741360305,
  "is_group": false
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (mesma cadeia de fallback que message.delivered)
message_ids []string UUIDs internos do Catcher (v4) das mensagens outbound confirmadas
chat string JID do chat (resolvido para phone JID quando possível)
sender string JID do dispositivo que confirmou (resolvido para phone JID quando possível)
chat_lid string LID original do chat, presente quando o WhatsApp usou LID internamente
sender_lid string LID original do sender, presente quando o WhatsApp usou LID internamente
status string Sempre "acked"
timestamp int64 Unix timestamp
is_group bool Se o chat e um grupo
group_name string Nome do grupo. Presente apenas quando is_group=true e o cache já foi populado (ver Mensagens de Grupo)
group_subject string Topico/descricao do grupo. Presente apenas quando is_group=true e o cache já foi populado

Este evento também pode trazer is_mutual / mutuality_checked_at — ver a nota de mutualidade no início desta seção.


message.read_other_device#

Disparado quando você le uma mensagem inbound em outro dispositivo seu. O contato não leu nada — e o seu próprio estado de leitura multi-device. Util para evitar re-notificar o operador de algo que ele já viu em outro aparelho. Não persistido (não carimba read_at, que e reservado para o receipt de leitura do contato sobre suas mensagens outbound).

json
{
  "phone": "554192464230",
  "message_ids": ["550e8400-e29b-41d4-a716-446655440000"],
  "chat": "554192464230@s.whatsapp.net",
  "sender": "554192464230@s.whatsapp.net",
  "chat_lid": "42812401807390@lid",
  "sender_lid": "42812401807390:77@lid",
  "status": "read_other_device",
  "timestamp": 1741360360,
  "is_group": false
}
Campo Tipo Descricao
phone string Número de telefone sem sufixo (mesma cadeia de fallback que message.delivered)
message_ids []string UUIDs internos do Catcher (v4) das mensagens inbound lidas
chat string JID do chat (resolvido para phone JID quando possível)
sender string JID do remetente da mensagem inbound (resolvido para phone JID quando possível)
chat_lid string LID original do chat, presente quando o WhatsApp usou LID internamente
sender_lid string LID original do sender, presente quando o WhatsApp usou LID internamente
status string Sempre "read_other_device"
timestamp int64 Unix timestamp
is_group bool Se o chat e um grupo
group_name string Nome do grupo. Presente apenas quando is_group=true e o cache já foi populado (ver Mensagens de Grupo)
group_subject string Topico/descricao do grupo. Presente apenas quando is_group=true e o cache já foi populado

Este evento também pode trazer is_mutual / mutuality_checked_at — ver a nota de mutualidade no início desta seção.


connection.diagnostic#

Timeline fina do ciclo de conexão — emitido em pontos-chave do ConnectInstance do worker (dialing, QR emitido, WebSocket fechado, etc.). Usado pela modal de QR do frontend para mostrar exatamente o que está acontecendo em tempo real. Não persistido. Carrega apenas dados seguros (IP do proxy, sinais de ciclo de vida do whatsmeow, mensagens curtas) — nunca URLs de CDN do Meta, telefones de terceiros ou conteúdo de mensagem.

json
{
  "step": "qr_emitted",
  "level": "info",
  "message": "QR Code gerado, aguardando leitura",
  "detail": {
    "proxy_city": "sao-paulo"
  },
  "timestamp": 1741360100
}
Campo Tipo Descricao
step string Slug estável do momento do ciclo de vida. Vocabulario: connect_start, proxy_bound, dialing, websocket_opened, qr_emitted, pair_device_received, pair_success, pair_error, connected, disconnected, websocket_closed, logged_out, connect_failure, stream_replaced, temp_ban, keepalive_timeout, keepalive_restored, reconnecting, flap_detected, manual_disconnect, quarantine_applied
level string info, success, warning ou error — controla a cor do badge na UI
message string Resumo de uma linha em pt-BR, legivel por humano
detail object Contexto estruturado opcional (IP do proxy, motivo da falha, etc.)
timestamp int64 Unix timestamp

contacts.bulk_imported#

Disparado quando o worker absorve um roster de contatos em massa de uma so vez — hoje apenas no HistorySync de INITIAL_BOOTSTRAP que segue um pareamento novo. CRMs podem usar isso como um único sinal "usuário acabou de parear, aqui estão os contatos" em vez de esperar o registro preencher incrementalmente. Não persistido como evento (os contatos em si vao para a tabela contacts).

json
{
  "source": "history_sync_initial_bootstrap",
  "imported": 312,
  "skipped": 4,
  "timestamp": 1741361000
}
Campo Tipo Descricao
source string Caminho do bulk-import. Hoje sempre "history_sync_initial_bootstrap"; reservado para caminhos futuros
imported int Quantidade de contatos absorvidos
skipped int Quantidade de contatos ignorados (já conhecidos ou inválidos). Omitido quando 0
timestamp int64 Unix timestamp

sync.offline_completed#

Disparado quando o whatsmeow termina de absorver eventos perdidos que chegaram durante uma janela de Disconnected. count > 0 significa que houve um gap real de entrega — a instância agora tem dados novos que o consumer não viu em tempo real. count == 0 significa que a reconexao cobriu o gap sem tráfego perdido (saudável). Não persistido.

json
{
  "count": 7,
  "timestamp": 1741361050
}
Campo Tipo Descricao
count int Número de eventos perdidos absorvidos. 0 = reconexao sem gap
timestamp int64 Unix timestamp

connection.recovered_quick#

Disparado quando um Connected segue um Disconnected dentro de uma janela curta (~60s). Distinto de instance.recovered, que exige >=15min de offline observavel. Util para dashboards que querem rastrear ruido de estabilidade de conexão separadamente de outages reais. Não persistido.

json
{
  "duration_seconds": 12.4,
  "timestamp": 1741361080
}
Campo Tipo Descricao
duration_seconds float64 Quanto tempo a instância ficou desconectada antes de voltar
timestamp int64 Unix timestamp

Persistencia de eventos#

Eventos são persistidos no banco de dados do tenant quando indicado na tabela de eventos. Eventos transientes (chat., label., privacy.update, blocklist.update, sync.*, média.retry_result, message.starred, message.deleted_for_me) não são gravados em MySQL — são entregues apenas via webhook e SSE/WebSocket.

Ciclo de vida da mensagem na tabela messages:

text
queued → sent → delivered → read
                    ↓
                 deleted (se revogada)
Campo Preenchido quando
queued_at Mensagem entra na fila
sent_at Enviada com sucesso ao WhatsApp
delivered_at Destinatário recebeu (receipt type=delivered)
read_at Destinatário leu (receipt type=read)
deleted_at Mensagem apagada (ProtocolMessage REVOKE)
failed_at Falha no envio

Tabelas de eventos:

Tabela Evento Descricao
messages message.received, message.sent, message.delivered, message.read, message.played, message.deleted, message.edited, message.undecryptable Todas as mensagens inbound/outbound com tracking completo
call_logs call.received, call.accepted, call.rejected, call.ended, call.group_offer Histórico de chamadas
group_events group.update, group.joined Auditoria de mudancas em grupos
contact_events contact.update, contact.identity_changed, contact.sync Mudancas em contatos