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",
"active": true,
"allowed_destination_ips": "203.0.113.10,2001:db8::/32"
}
| 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 |
active |
boolean | não | Default true. Envie false para criar o webhook pausado. |
allowed_destination_ips |
string | não | Allowlist separada por virgula de IPv4/IPv6/CIDR para defesa contra DNS rebinding. Vazio/omitido = sem restrição. |
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 | Não cria linha em messages; salvo como evento operacional |
message.decrypt_recovered |
Mensagem antes indecriptável recuperada após retry | Não cria linha extra em messages; salvo como evento operacional |
message.decrypt_lost |
Mensagem indecriptável expirada sem recuperação | Não cria linha em messages; salvo como evento operacional |
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 (caller-side) | Sim (tabela call_logs) |
call.declined_by_me |
Você (instância) recusou uma chamada de entrada (UI do telefone ou auto-rejeição) | Não cria nova linha — atualiza call_logs.end_reason via call.ended |
call.ended |
Chamada finalizada (qualquer motivo: timeout, hangup, rejeição) | 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,
"company_id": 10,
"instance_id": "84c2e480-...",
"url": "https://meuservidor.com/webhook",
"secret": "a1b2c3d4e5f6...64_hex_chars",
"events": "message.received,message.sent",
"active": true,
"allowed_destination_ips": "203.0.113.10,2001:db8::/32",
"created_at": "2026-03-07T10:00:00Z",
"updated_at": "2026-03-07T10:00:00Z"
}
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; secret informado com menos de 16 caracteres; ou allowed_destination_ips inválido |
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
Query Parameters:
| Parametro | Tipo | Padrão | Descricao |
|---|---|---|---|
instance_id |
string | — | Filtra webhooks dessa instância. Para tenants, retorna webhooks da própria empresa com instance_id igual ao informado OU globais. Para superadmin, faz lookup cross-company apenas por instance_id. |
Resposta 200:
[
{
"id": 1,
"company_id": 10,
"instance_id": "84c2e480-...",
"url": "https://meuservidor.com/webhook",
"events": "message.received,message.sent",
"active": true,
"allowed_destination_ips": "",
"created_at": "2026-03-07T10:00:00Z",
"updated_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",
"allowed_destination_ips": ""
}
Todos os campos são opcionais. Em allowed_destination_ips, campo omitido preserva o valor atual; string vazia limpa a allowlist; valor não vazio substitui após validação.
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": "{...}",
"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"],
"whatsapp_message_ids": ["3EB0ABC123DEF456"],
"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. |
whatsapp_message_ids |
[]string | ID(s) bruto(s) do WhatsApp que originaram o evento. Use apenas para auditoria/forense/correlação de baixo nível; para APIs e webhooks de negócio, prefira message_ids |
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"],
"whatsapp_message_ids": ["3EB0DEF456ABC123"],
"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. |
whatsapp_message_ids |
[]string | ID(s) bruto(s) do WhatsApp retornados pelo envio. Útil para auditoria e comparação com traces whatsmeow; não substitui o UUID Catcher em message_ids |
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",
"whatsapp_message_ids": ["3EB0DEF456ABC123", "3EB0DEF456ABC124"],
"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. |
whatsapp_message_ids |
[]string | ID(s) bruto(s) do WhatsApp recebidos no receipt. Pode ter múltiplos elementos quando o WhatsApp agrega receipts |
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",
"whatsapp_message_ids": ["3EB0DEF456ABC123"],
"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. |
whatsapp_message_ids |
[]string | ID(s) bruto(s) do WhatsApp recebidos no receipt |
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",
"whatsapp_message_ids": ["3EB0DEF456ABC123"],
"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. |
whatsapp_message_ids |
[]string | ID(s) bruto(s) do WhatsApp recebidos no receipt |
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",
"whatsapp_message_ids": ["3EB0ABC123DEF456"],
"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. |
whatsapp_message_ids |
[]string | ID bruto do WhatsApp da mensagem alvo da revogação. Presente mesmo quando o UUID Catcher não foi encontrado |
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",
"whatsapp_message_ids": ["3EB0ABC123DEF456"],
"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. |
whatsapp_message_ids |
[]string | ID bruto do WhatsApp da mensagem alvo da edição. Presente mesmo quando o UUID Catcher não foi encontrado |
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: o rollup superadmin de saúde das instâncias retorna a visão cross-tenant em tempo real (usado pelo banner do dashboard, badge da sidebar, e card "Saúde das instâncias").
instance.critical_offline#
Mesmo payload de instance.offline / instance.critical_offline / instance.recovered; tier vem como critical_offline.
instance.recovered#
Mesmo payload de instance.offline / instance.critical_offline / instance.recovered; tier volta para o estado saudável/stale atual e previous_tier aponta para offline ou critical_offline.
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. Não cria uma linha normal em messages, porque o conteúdo não foi aberto; o evento fica disponível em webhooks/SSE/WS e no histórico de eventos.
{
"phone": "554192464230",
"from": "554192464230@s.whatsapp.net",
"chat": "554192464230@s.whatsapp.net",
"message_ids": ["3EB0ABC123DEF456"],
"whatsapp_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 carrega ID do WhatsApp e não UUID Catcher, porque a mensagem não pode ser persistida. |
whatsapp_message_ids |
[]string | Mesmo ID bruto do WhatsApp, exposto explicitamente para correlação com traces e eventos message.decrypt_recovered / message.decrypt_lost |
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.decrypt_recovered#
Disparado quando uma mensagem que antes gerou message.undecryptable chega novamente e é decriptada com sucesso pelo retry automático do WhatsApp/whatsmeow.
{
"phone": "554192464230",
"from": "554192464230@s.whatsapp.net",
"chat": "554192464230@s.whatsapp.net",
"message_ids": ["3EB0ABC123DEF456"],
"whatsapp_message_ids": ["3EB0ABC123DEF456"],
"status": "recovered",
"recovery_ms": 831,
"timestamp": 1741360801
}
message.decrypt_lost#
Disparado quando uma mensagem que gerou message.undecryptable não reaparece dentro da janela de retry. Esse evento indica perda real de conteúdo, não apenas ruído transitório.
{
"phone": "554192464230",
"from": "554192464230@s.whatsapp.net",
"chat": "554192464230@s.whatsapp.net",
"message_ids": ["3EB0ABC123DEF456"],
"whatsapp_message_ids": ["3EB0ABC123DEF456"],
"status": "lost",
"ttl_secs": 300,
"timestamp": 1741361100
}
Campos adicionais opcionais nos dois eventos: sender_name, is_group, group_name.
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"],
"whatsapp_message_ids": ["3EB0ABC123DEF456"],
"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. |
whatsapp_message_ids |
[]string | ID bruto do WhatsApp da mensagem marcada/desmarcada |
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"],
"whatsapp_message_ids": ["3EB0ABC123DEF456"],
"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. |
whatsapp_message_ids |
[]string | ID bruto do WhatsApp da mensagem apagada localmente |
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. Valores observados: "reject" (você recusou — também dispara call.declined_by_me), "timeout" (não atendeu), "" (caller desligou), "upgrade-needed" (versão do cliente desatualizada), entre outros. Lista abaixo da seção. |
duration |
int | Duração da chamada em segundos (0 se não atendida) |
timestamp |
int64 | Unix timestamp |
call.declined_by_me#
Disparado quando você (instância) recusa uma chamada de entrada. Há duas origens distintas, distinguidas pelo campo source:
- Manual — o usuário tocou em recusar no app do WhatsApp do telefone pareado
- Automática — o toggle
auto_reject_callsda instância estava ativo no momento da oferta
Por que existe este evento. No protocolo do WhatsApp NÃO há um frame dedicado para "usuário recusou no telefone". O sinal canônico no fio é o
CallTerminatecujo atributofromflipa para o JID/LID do PRÓPRIO dispositivo que rejeitou (em vez do JID do chamador original). Em logs de produção, oreasondesse terminate vem vazio ("") — não"reject"como Baileys documenta para casos legados. A Catcher detecta os dois sinais (self-JID/LID match OU reason="reject") e recupera o JID do chamador original via stash interno populado noCallOffer. O eventocall.endedcontinua disparando em paralelo carregando o mesmophone/fromrecuperado.
{
"phone": "554192464230",
"from": "554192464230@s.whatsapp.net",
"call_id": "CALL-ABC123",
"source": "manual",
"timestamp": 1741361050
}
| Campo | Tipo | Descricao |
|---|---|---|
phone |
string | Número de telefone do chamador |
from |
string | JID do chamador |
call_id |
string | ID único da chamada |
source |
string | "manual" (recusa na UI do telefone) ou "auto" (toggle auto_reject_calls ativo) |
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": "contacts",
"status": "contacts",
"read_receipts": "enabled",
"call_add": "known"
},
"changed_fields": ["group_add", "profile"],
"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,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 |
media.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 |
Payloads adicionais de eventos#
Os eventos abaixo também fazem parte de AllEventTypes. Os exemplos usam o envelope real {type,data} e os campos vêm dos *Data structs em whats-shared/pkg/realtime/event.go.
status.received#
Campos: phone, from, status_message_id, type, content, media_id, mime_type, timestamp.
{ "type": "status.received", "data": { "phone": "554137984905", "from": "554137984905@s.whatsapp.net", "status_message_id": "3EB0STATUS", "type": "text", "content": "Aberto hoje", "timestamp": 1741361650 } }
status.viewed#
Campos: phone, from, status_message_id, whatsapp_status_id, status_type, receipt_type, timestamp.
{ "type": "status.viewed", "data": { "phone": "554137984905", "from": "554137984905@s.whatsapp.net", "status_message_id": "550e8400-e29b-41d4-a716-446655440000", "whatsapp_status_id": "3EB0STATUS", "status_type": "image", "receipt_type": "read", "timestamp": 1741361650 } }
status.reaction#
Campos: phone, from, push_name, status_message_id, whatsapp_status_id, status_type, emoji, removed, timestamp.
{ "type": "status.reaction", "data": { "phone": "554137984905", "from": "554137984905@s.whatsapp.net", "push_name": "Joao", "status_message_id": "550e8400-e29b-41d4-a716-446655440000", "whatsapp_status_id": "3EB0STATUS", "status_type": "image", "emoji": "👍", "removed": false, "timestamp": 1741361650 } }
status.reply#
Campos: phone, from, push_name, status_message_id, whatsapp_status_id, status_type, reply_message_id, whatsapp_reply_id, reply_type, content, media_id, timestamp.
{ "type": "status.reply", "data": { "phone": "554137984905", "from": "554137984905@s.whatsapp.net", "push_name": "Joao", "status_message_id": "550e8400-e29b-41d4-a716-446655440000", "whatsapp_status_id": "3EB0STATUS", "reply_message_id": "550e8400-e29b-41d4-a716-446655440001", "whatsapp_reply_id": "3EB0REPLY", "reply_type": "text", "content": "Gostei", "timestamp": 1741361650 } }
message.pin_sent#
Campos: chat, phone, target_message_id, target_whatsapp_message_id, envelope_whatsapp_message_id, pin, duration_seconds, target_from_me, timestamp.
{ "type": "message.pin_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "target_message_id": "550e8400-e29b-41d4-a716-446655440000", "target_whatsapp_message_id": "3EB0TARGET", "envelope_whatsapp_message_id": "3EB0PIN", "pin": true, "duration_seconds": 86400, "target_from_me": true, "timestamp": 1741361650 } }
message.poll_vote_sent#
Campos: chat, phone, poll_message_id, poll_whatsapp_message_id, envelope_whatsapp_message_id, selected_options, poll_from_me, timestamp.
{ "type": "message.poll_vote_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "poll_message_id": "550e8400-e29b-41d4-a716-446655440000", "poll_whatsapp_message_id": "3EB0POLL", "envelope_whatsapp_message_id": "3EB0VOTE", "selected_options": ["sim"], "poll_from_me": false, "timestamp": 1741361650 } }
message.star_sent#
Campos: chat, phone, message_id, whatsapp_message_id, starred, from_me, timestamp.
{ "type": "message.star_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "message_id": "550e8400-e29b-41d4-a716-446655440000", "whatsapp_message_id": "3EB0ABC123", "starred": true, "from_me": true, "timestamp": 1741361650 } }
message.read_sent#
Campos: chat, phone, message_ids, whatsapp_message_ids, count, timestamp.
{ "type": "message.read_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "message_ids": ["550e8400-e29b-41d4-a716-446655440000"], "whatsapp_message_ids": ["3EB0ABC123"], "count": 1, "timestamp": 1741361650 } }
instance.no_proxy_blocked#
Campos: reason, detail, blocked_at, desired_state.
{ "type": "instance.no_proxy_blocked", "data": { "reason": "proxy_required", "detail": "instance has no proxy binding", "blocked_at": "2026-05-21T12:00:00Z", "desired_state": "DISCONNECTED" } }
contact.block_sent#
Campos: phone, jid, timestamp.
{ "type": "contact.block_sent", "data": { "phone": "554137984905", "jid": "554137984905@s.whatsapp.net", "timestamp": 1741361650 } }
contact.unblock_sent#
Campos: phone, jid, timestamp.
{ "type": "contact.unblock_sent", "data": { "phone": "554137984905", "jid": "554137984905@s.whatsapp.net", "timestamp": 1741361650 } }
contact.presence_subscribed#
Campos: phone, jid, timestamp.
{ "type": "contact.presence_subscribed", "data": { "phone": "554137984905", "jid": "554137984905@s.whatsapp.net", "timestamp": 1741361650 } }
profile.name_changed_sent#
Campos: new_name, timestamp.
{ "type": "profile.name_changed_sent", "data": { "new_name": "Empresa LTDA", "timestamp": 1741361650 } }
profile.picture_changed_sent#
Campos: bytes_uploaded, timestamp.
{ "type": "profile.picture_changed_sent", "data": { "bytes_uploaded": 43821, "timestamp": 1741361650 } }
chat.unread_sent#
Campos: chat, phone, timestamp.
{ "type": "chat.unread_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "timestamp": 1741361650 } }
chat.archive_sent#
Campos: chat, phone, archived, timestamp. Disparado quando você arquiva/desarquiva um chat. É uma mutação de AppState que não gera self-echo, então este é o único sinal de que você fez a ação.
{ "type": "chat.archive_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "archived": true, "timestamp": 1741361650 } }
chat.pin_sent#
Campos: chat, phone, pinned, timestamp. Disparado quando você fixa/desfixa um chat.
{ "type": "chat.pin_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "pinned": true, "timestamp": 1741361650 } }
chat.mute_sent#
Campos: chat, phone, muted, mute_end (opcional), timestamp. Disparado quando você muta/desmuta um chat. mute_end é o unix de expiração do mute (ausente ou 0 = indefinido / desmutado).
{ "type": "chat.mute_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "muted": true, "mute_end": 1741448050, "timestamp": 1741361650 } }
chat.clear_sent#
Campos: chat, phone, timestamp. Disparado quando você limpa as mensagens de um chat MANTENDO o chat na lista (distinto de chat.delete_sent).
{ "type": "chat.clear_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "timestamp": 1741361650 } }
chat.delete_sent#
Campos: chat, phone, timestamp. Disparado quando você deleta um chat (remove da lista de conversas).
{ "type": "chat.delete_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "timestamp": 1741361650 } }
chat.disappearing_timer_sent#
Campos: chat, phone, duration_seconds, timestamp. Disparado quando você define o timer de mensagens temporárias do chat. duration_seconds ∈ 0 (desligado) | 86400 (24h) | 604800 (7 dias) | 7776000 (90 dias).
{ "type": "chat.disappearing_timer_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "duration_seconds": 604800, "timestamp": 1741361650 } }
status.reply_sent#
Campos: chat, phone, status_message_id, type, timestamp. Disparado quando você responde ao status de um contato (lado executor; a contraparte inbound no autor do status é status.reply). chat = JID 1:1 do autor (onde a resposta cai); status_message_id = id WhatsApp do status respondido; type ∈ text | image | video | sticker.
{ "type": "status.reply_sent", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "status_message_id": "3EB0STATUS", "type": "text", "timestamp": 1741361650 } }
presence.broadcast_sent#
Campos: presence_type, chat, phone, timestamp.
{ "type": "presence.broadcast_sent", "data": { "presence_type": "available", "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "timestamp": 1741361650 } }
group.create_sent#
Campos: group_jid, name, participants_jids, participant_count, timestamp.
{ "type": "group.create_sent", "data": { "group_jid": "120363012345678901@g.us", "name": "Equipe Vendas", "participants_jids": ["554137984905@s.whatsapp.net"], "participant_count": 1, "timestamp": 1741361650 } }
group.name_changed_sent#
Campos: group_jid, new_name, timestamp.
{ "type": "group.name_changed_sent", "data": { "group_jid": "120363012345678901@g.us", "new_name": "Novo Nome", "timestamp": 1741361650 } }
group.description_changed_sent#
Campos: group_jid, new_description, timestamp.
{ "type": "group.description_changed_sent", "data": { "group_jid": "120363012345678901@g.us", "new_description": "Nova descricao", "timestamp": 1741361650 } }
group.picture_changed_sent#
Campos: group_jid, bytes_uploaded, timestamp.
{ "type": "group.picture_changed_sent", "data": { "group_jid": "120363012345678901@g.us", "bytes_uploaded": 43821, "timestamp": 1741361650 } }
group.participants_add_sent#
Campos: group_jid, action, requested_jids, successful_jids, failed_jids, timestamp.
{ "type": "group.participants_add_sent", "data": { "group_jid": "120363012345678901@g.us", "action": "add", "requested_jids": ["554137984905@s.whatsapp.net"], "successful_jids": ["554137984905@s.whatsapp.net"], "failed_jids": [], "timestamp": 1741361650 } }
group.participants_remove_sent#
Campos iguais a group.participants_add_sent, com action="remove".
{ "type": "group.participants_remove_sent", "data": { "group_jid": "120363012345678901@g.us", "action": "remove", "requested_jids": ["554137984905@s.whatsapp.net"], "successful_jids": ["554137984905@s.whatsapp.net"], "failed_jids": [], "timestamp": 1741361650 } }
group.participants_promote_sent#
Campos iguais a group.participants_add_sent, com action="promote".
{ "type": "group.participants_promote_sent", "data": { "group_jid": "120363012345678901@g.us", "action": "promote", "requested_jids": ["554137984905@s.whatsapp.net"], "successful_jids": ["554137984905@s.whatsapp.net"], "failed_jids": [], "timestamp": 1741361650 } }
group.participants_demote_sent#
Campos iguais a group.participants_add_sent, com action="demote".
{ "type": "group.participants_demote_sent", "data": { "group_jid": "120363012345678901@g.us", "action": "demote", "requested_jids": ["554137984905@s.whatsapp.net"], "successful_jids": ["554137984905@s.whatsapp.net"], "failed_jids": [], "timestamp": 1741361650 } }
group.participants_approved_sent#
Campos iguais a group.participants_add_sent, com action="approve".
{ "type": "group.participants_approved_sent", "data": { "group_jid": "120363012345678901@g.us", "action": "approve", "requested_jids": ["554137984905@s.whatsapp.net"], "successful_jids": ["554137984905@s.whatsapp.net"], "failed_jids": [], "timestamp": 1741361650 } }
group.participants_rejected_sent#
Campos iguais a group.participants_add_sent, com action="reject".
{ "type": "group.participants_rejected_sent", "data": { "group_jid": "120363012345678901@g.us", "action": "reject", "requested_jids": ["554137984905@s.whatsapp.net"], "successful_jids": ["554137984905@s.whatsapp.net"], "failed_jids": [], "timestamp": 1741361650 } }
group.settings_sent#
Campos: group_jid, announce, locked, timestamp.
{ "type": "group.settings_sent", "data": { "group_jid": "120363012345678901@g.us", "announce": true, "locked": false, "timestamp": 1741361650 } }
group.invite_link_revoked_sent#
Campos: group_jid, new_link, timestamp.
{ "type": "group.invite_link_revoked_sent", "data": { "group_jid": "120363012345678901@g.us", "new_link": "https://chat.whatsapp.com/AbCdEfGhIjKl", "timestamp": 1741361650 } }
group.join_sent#
Campos: group_jid, invite_code, timestamp.
{ "type": "group.join_sent", "data": { "group_jid": "120363012345678901@g.us", "invite_code": "AbCdEfGhIjKl", "timestamp": 1741361650 } }
group.leave_sent#
Campos: group_jid, timestamp.
{ "type": "group.leave_sent", "data": { "group_jid": "120363012345678901@g.us", "timestamp": 1741361650 } }
instance.deleted#
Campos: instance_id, phone, timestamp.
{ "type": "instance.deleted", "data": { "instance_id": "84c2e480-...", "phone": "554137984905", "timestamp": 1741361650 } }
history.import_started#
Campos: oldest_message_id, limit, timestamp.
{ "type": "history.import_started", "data": { "oldest_message_id": "3EB0OLD", "limit": 500, "timestamp": 1741361650 } }
history.import_completed#
Campos: imported_count, limit, duration_ms, timestamp.
{ "type": "history.import_completed", "data": { "imported_count": 342, "limit": 500, "duration_ms": 1842, "timestamp": 1741361650 } }
message.pinned#
Campos: chat, phone, sender, is_group, envelope_whatsapp_message_id, target_whatsapp_message_id, target_from_me, sender_timestamp_ms, timestamp.
{ "type": "message.pinned", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "sender": "554137984905@s.whatsapp.net", "is_group": false, "envelope_whatsapp_message_id": "3EB0PIN", "target_whatsapp_message_id": "3EB0TARGET", "target_from_me": true, "timestamp": 1741361650 } }
message.unpinned#
Mesmo payload de message.pinned, para unpin.
{ "type": "message.unpinned", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "sender": "554137984905@s.whatsapp.net", "is_group": false, "envelope_whatsapp_message_id": "3EB0PIN", "target_whatsapp_message_id": "3EB0TARGET", "target_from_me": true, "timestamp": 1741361650 } }
message.poll_vote_received#
Campos: chat, phone, voter, is_group, envelope_whatsapp_message_id, poll_whatsapp_message_id, poll_from_me, timestamp.
{ "type": "message.poll_vote_received", "data": { "chat": "120363012345678901@g.us", "phone": "554137984905", "voter": "554137984905@s.whatsapp.net", "is_group": true, "envelope_whatsapp_message_id": "3EB0VOTE", "poll_whatsapp_message_id": "3EB0POLL", "poll_from_me": false, "timestamp": 1741361650 } }
message.reaction_received#
Campos: chat, phone, sender, is_group, envelope_whatsapp_message_id, target_whatsapp_message_id, target_from_me, emoji, timestamp.
{ "type": "message.reaction_received", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "sender": "554137984905@s.whatsapp.net", "is_group": false, "envelope_whatsapp_message_id": "3EB0REACTION", "target_whatsapp_message_id": "3EB0TARGET", "target_from_me": true, "emoji": "👍", "timestamp": 1741361650 } }
message.product_received#
Campos: chat, phone, sender, is_group, envelope_whatsapp_message_id, product_id, business_owner_jid, title, timestamp.
{ "type": "message.product_received", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "sender": "554137984905@s.whatsapp.net", "is_group": false, "envelope_whatsapp_message_id": "3EB0PROD", "product_id": "prod_123", "business_owner_jid": "554100000000@s.whatsapp.net", "title": "Produto", "timestamp": 1741361650 } }
message.order_received#
Campos: chat, phone, sender, is_group, envelope_whatsapp_message_id, order_id, merchant_jid, item_count, total_amount_1000, currency, timestamp.
{ "type": "message.order_received", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "sender": "554137984905@s.whatsapp.net", "is_group": false, "envelope_whatsapp_message_id": "3EB0ORDER", "order_id": "order_123", "merchant_jid": "554100000000@s.whatsapp.net", "item_count": 2, "total_amount_1000": 9900000, "currency": "BRL", "timestamp": 1741361650 } }
message.decrypt_retry_pending#
Campos: chat, phone, sender, is_group, whatsapp_message_id, is_unavailable, unavailable_type, decrypt_fail_mode, started_at, retry_deadline, retry_window_seconds.
{ "type": "message.decrypt_retry_pending", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "sender": "554137984905@s.whatsapp.net", "is_group": false, "whatsapp_message_id": "3EB0BAD", "is_unavailable": true, "unavailable_type": "unavailable", "decrypt_fail_mode": "retry", "started_at": "2026-05-21T12:00:00Z", "retry_deadline": "2026-05-21T12:02:00Z", "retry_window_seconds": 120 } }
session.recreated#
Campos: peer_jid, phone, retry_count, reason, timestamp.
{ "type": "session.recreated", "data": { "peer_jid": "554137984905@s.whatsapp.net", "phone": "554137984905", "retry_count": 2, "reason": "retry_count_high", "timestamp": 1741361650 } }
message.delivery_failed#
Campos: chat, phone, message_id, whatsapp_message_id, idempotency_key, task_id, message_type, reason, last_error, attempt_count, timestamp.
{ "type": "message.delivery_failed", "data": { "chat": "554137984905@s.whatsapp.net", "phone": "554137984905", "message_id": "550e8400-e29b-41d4-a716-446655440000", "idempotency_key": "send-001", "task_id": "asynq:task:abc", "message_type": "text", "reason": "retry_exhausted", "last_error": "timeout", "attempt_count": 5, "timestamp": 1741361650 } }
instance.qr_scanned#
Campos: phone, device_jid, business_name, platform_name, timestamp.
{ "type": "instance.qr_scanned", "data": { "phone": "554137984905", "device_jid": "554137984905:1@s.whatsapp.net", "business_name": "Empresa LTDA", "platform_name": "smbi", "timestamp": 1741361650 } }
instance.proxy_changed#
Campos: old_ip, new_ip, reason, old_city, new_city, provider, timestamp.
{ "type": "instance.proxy_changed", "data": { "old_ip": "198.51.100.10", "new_ip": "198.51.100.11", "reason": "health_replace", "old_city": "br-saopaulo", "new_city": "br-saopaulo", "provider": "brightdata", "timestamp": 1741361650 } }
contact.temperature_changed#
Campos: remote_jid, phone, old_tier, new_tier, old_score, new_score, direction, trigger, timestamp.
{ "type": "contact.temperature_changed", "data": { "remote_jid": "554137984905@s.whatsapp.net", "phone": "554137984905", "old_tier": "cold", "new_tier": "warm", "old_score": 25, "new_score": 42, "direction": "inbound", "trigger": "message.received", "timestamp": 1741361650 } }
anti_spam.blocked#
Campos: remote_jid, phone, rule, detail, message_type, bypass_available, bypass_quota_left, content_hash, timestamp.
{ "type": "anti_spam.blocked", "data": { "remote_jid": "554137984905@s.whatsapp.net", "phone": "554137984905", "rule": "duplicate_content", "detail": "same content sent in rolling 24h", "message_type": "text", "bypass_available": true, "bypass_quota_left": 2, "content_hash": "sha256:abc", "timestamp": 1741361650 } }
whatsapp.policy_warning#
Campos: warning_code, category, message, expires_at, severity_hint, recommended_action, timestamp.
{ "type": "whatsapp.policy_warning", "data": { "warning_code": "rate_limit", "category": "anti_abuse", "message": "rate limit warning", "expires_at": "2026-05-21T13:00:00Z", "severity_hint": "warn", "recommended_action": "slow_down", "timestamp": 1741361650 } }
Persistencia de eventos#
Eventos são persistidos no histórico operacional do tenant quando passam pelo pipeline de realtime/webhooks. Eventos transientes (chat., label., privacy.update, blocklist.update, sync.*, media.retry_result, message.starred, message.deleted_for_me, message.undecryptable, message.decrypt_recovered, message.decrypt_lost) não criam/alteram linhas de mensagem em messages — são entregues via webhook/SSE/WebSocket e consultáveis pelo histórico de eventos quando habilitado.
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 | 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 |