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:
{
"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_idpara 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:
{
"id": 1,
"url": "https://meuservidor.com/webhook",
"secret": "a1b2c3d4e5f6...64_hex_chars",
"events": "message.received,message.sent",
"active": true
}
O
secrete 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. Osecretaparece somente na resposta de criação.GET,LISTePATCHnunca o reexibem. A entrega ehttpsonly por padrão. Em runtime você pode afrouxar isso comWEBHOOK_ALLOW_INSECURE_HTTP=trueou restringir dominios comWEBHOOK_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:
[
{
"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:
{
"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:
{
"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:
{
"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:
{
"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=trueindica que o evento bateu emSkipRetrypor serpermanent_4xxapós 3 tentativas — provavelmente um bug de configuração (URL errada, secret expirado, parser quebrado).has_payload_raw=falseindica que opayload_rawfoi 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:
{
"log_ids": [123, 124, 125]
}
Modo 2 — por filtro:
{
"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:
{
"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):
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_idporque a entrega eat-least-once
Headers enviados pelo Catcher:
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):
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):
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:
{
"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 (tipicamenteeventem vez detype). A correção e no consumer: leiabody.type(nãobody.event). Cheque também se o consumer usaexpress.raw()(ou equivalente) para preservar os bytes exatos do body —JSON.parse+JSON.stringifymuda 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 umphonetambém podem trazeris_mutual(bool) emutuality_checked_at(string ISO-8601).is_mutual=truesignifica que o contato resolvido tem a instância salva na agenda dele (lido do cachecontact_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.
{
"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:
{
"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:
chate o JID do grupo (@g.us),from/phone/push_nameidentificam o participante que enviou, egroup_name/group_subjectdescrevem o grupo. Veja Mensagens de Grupo para o padrão completo.
Exemplo com contexto de anuncio (Instagram/Facebook click-to-WhatsApp):
{
"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):
{
"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/contactcomcontacts: [...]) produz umContactsArrayMessage: o mesmo eventotype=contactcom uma entrada emcontactspor 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 (quandoS3_PUBLIC_URLestá 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 camposource_urldo proto Meta (permalink do post Instagram/Facebook) permanece intencionalmente removido — consumidores podem reconstruir referência viasource_id+source_typequando 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
phoneidentifica quem enviou o cartao, não o contato compartilhado. Para os números do(s) contato(s) compartilhado(s), usecontacts[].phones.
Reactions: Quando
type=reaction, o campocontentcontem o emoji (ex:"❤️"). Umcontentvazio indica que a reaction foi removida. O camporeaction_target_idcontem 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}.
{
"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):
{
"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
}
contentem type=contact: com um único contato, e odisplay_name. Com varios, e a juncao dosdisplay_nameseparados por", "(ex:"Suporte Catcher, Comercial"). Para os dados estruturados, sempre usecontacts[].
| 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.sentcomsource: "external". Isso permite rastrear toda a comunicação outbound independente de onde foi originada. Mensagens enviadas pela própria Catcher via fila temsource: "api". A persistencia no banco também distingue: a colunasourcena tabelamessagesarmazena"api"ou"external". Em mensagens externas,quoted_msg_idereaction_target_idtambém são preservados quando presentes, inclusive paramessage.sentde 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:
- RecipientAlt (protocolo): o WhatsApp envia
peer_recipient_pnno evento, contendo o telefone do destinatário — está e a fonte primaria e mais confiavel para outbound echo.- MySQL (
contact_identities): registro persistido de interacoes anteriores (SenderAlt, RecipientAlt, queries de contato).- 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,toechatconterao o telefone resolvido. Caso contrario,phoneficara vazio echat_lidcontera o LID original — quando o contato responder (trazendoSenderAlt) 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.
{
"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.
{
"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.
{
"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).
{
"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).
{
"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.
{
"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.
{
"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 atualizadastream_error— erro no stream de comunicação com o servidor WhatsAppkeepalive_timeout— keepalive não recebeu resposta a tempo; conexão pode estar instávelkeepalive_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ósPOST /v1/instances/{instanceId}/contacts/{contactId}/presence/subscribe.
Snapshot opcional:
GET /v1/instances/{instanceId}/contacts/{contactId}/presencele 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.
{
"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.
{
"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_namecomo titulo da conversa epush_namecomo autor do balao individual.chate 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:
- Primeira mensagem de um grupo novo: o cache está vazio. O evento e emitido com
group_name=""egroup_subject="". Em paralelo, a Catcher dispara uma busca assincrona viaGetGroupInfopara popular o cache. - Mensagens subsequentes: o cache já tem o nome, entao todos os eventos do mesmo grupo virao com
group_nameegroup_subjectpreenchidos. - Eventos
events.GroupInfocomname_changeoutopic_changeatualizam o cache imediatamente, antes mesmo de emitir ogroup.updatecorrespondente. - 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.
{
"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
actionepicture_change, a Catcher automaticamente baixa a nova foto do contato e armazena no S3/R2. Quandoactionepicture_remove, o cache e limpo. Clientes podem usar este evento para invalidar avatares em cache local e buscar a nova versão viaGET /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.
{
"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.updatecomstatus: "banned"também e emitido. Useinstance.bannedquando 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 tierdegradedparaoffline).instance.critical_offline— instância desconectada ha 6h ou mais (transicao do tierofflineparacritical_offline). Também dispara email para o owner da empresa + todos os superadmins.instance.recovered— instância voltou a conectar após ter ficado emoffline/critical_offline(limpa ooffline_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.
{
"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, editeOfflineThresholdeCriticalThreshold.
Endpoint relacionado:
GET /v1/admin/instance-healthretorna 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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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
actione"set", a listachangesrepresenta a lista completa de contatos bloqueados (todos comaction: "block"). Quando e"modify", cada item emchangespode 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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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 oSendAppStatebem-sucedido, sem esperar o echo de sincronização do WhatsApp.
chat.clear#
Disparado quando um chat e limpo (histórico apagado). Não persistido.
{
"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.
{
"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.
{
"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.
{
"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.sentpor mensagem, para evitar que consumidores processem mensagens antigas como tráfego novo. Usehistory.syncpara progresso/resumo eGET /chats/{chatId}/messagespara 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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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).
{
"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).
{
"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.
{
"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).
{
"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.
{
"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.
{
"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:
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 |