Payloads de eventos
Todo corpo de webhook tem o mesmo envelope:
{
"api_version": "v1",
"event_id": "9bc38acc-2edc-482e-93d4-0482b432ffa6",
"event": "message.received",
"customer_id": "2696bc9b-2e37-43d9-a33b-5a79f81daeba",
"channel_id": "8d653c66-e4ff-43ee-97da-3de5ad5680d4",
"created_at": "2026-05-01T12:34:56.000Z",
"data": { /* formato específico do evento, veja abaixo */ }
}Os campos do envelope:
| Campo | Tipo | Notas |
|---|---|---|
api_version | string | Hoje é sempre "v1". Compare exatamente com isso se você validar a versão. |
event_id | UUID v4 | Estável entre retentativas da mesma entrega. Use como sua chave de deduplicação. |
event | string | O nome do evento. Veja a lista abaixo. |
customer_id | UUID | O id da sua conta. Sempre presente. |
channel_id | UUID | O canal que produziu o evento. Sempre presente. |
created_at | ISO-8601 UTC | Horário do relógio no gateway quando o evento foi emitido. |
data | objeto | Payload específico do evento, schemas abaixo. |
Os cinco campos escalares são obrigatórios — nenhum é nulo. data é
obrigatório e sempre um objeto; o formato dele é decidido por event.
Não existe campo occurred_at, e os ids são UUID, não ULID — revisões
anteriores desta página documentavam os dois. Um receptor escrito a partir
daquilo lê undefined no timestamp.
Tipos de evento
Todos os 25 eventos. Assinar um nome fora desta lista é rejeitado na
criação do endpoint com 400 invalid_request.
Mensagens
message.received— Mensagem recebida de um contato. Carregafrom,to,chat_id,timestamp,is_group,body, e opcionalmentepush_name. Veja a nota sobre resolução de LID abaixo.message.sent— Sua mensagem de saída chegou ao WhatsApp. Carregamessage_id,to,timestamp,body, e o seuclient_ref.message.status— Um tique de entrega para uma mensagem que você enviou:sent→delivered→read/played, oufailed. Veja abaixo.message.reaction— Alguém reagiu numa conversa em que seu canal está. Carregamessage_key,reactor,emoji,timestamp. Umemojivazio significa reação removida.
Ciclo de vida do canal
channel.status— Toda transição de ciclo de vida, num evento só. Carregastatus,previous_status,changed_at,simple(qr/connected/logged_out/stopped),liveecan_send. Se você conduz o pareamento sem interface, assine este e ignore os três abaixo.simpleé sinal de pareamento, não de saúde — um canaldisconnectedreportasimple: "connected"de propósito, porque as credenciais seguem boas e o socket volta sozinho. Leialivepara "o socket está de pé agora" ecan_sendpara "um envio vai ser aceito".channel.connected— O canal terminou o pareamento. Carregaphone_number,connected_at.channel.disconnected— A sessão terminou. Carregareason,will_reconnect,disconnected_at.qr.updated— Um novo QR está disponível. Carregaqr_png_base64(base64 pura) eexpires_at(epoch ms).
Grupos
group.created— Carregagroup_id,by,timestamp,subject,participants.group.updated— Metadados do grupo mudaram.group.participant_added/group.participant_removed— Composição mudou.group.admin_promoted/group.admin_demoted— Direitos de admin mudaram.
Os seis carregam group_id, o ator by (pode ser nulo), e timestamp.
Chats
chat.updated— Estado do chat mudou (mute, não-lidas, …).chat.archived— Chat arquivado ou desarquivado.chat.pinned— Chat fixado ou desafixado.
Comércio (apenas contas WhatsApp Business)
order.placed/order.cancelled— Um pedido de entrada mudou de estado.cart.updated— O carrinho de um cliente mudou.
Outros
story.viewed— Alguém viu um story que você publicou.call.offered/call.terminated— Ciclo de vida de chamada recebida.presence.updated— A presença de um contato mudou. Carregachat_id,contact_id,presence(available/unavailable/composing/recording/paused) elast_seen(segundos Unix, ounullquando a privacidade esconde).contact.resolved— Dispara na primeira vez que descobrimos o JID de telefone por trás de um contato antes conhecido apenas por LID. Veja abaixo.
Não existe evento message.delivered nem message.failed. Os dois são
frames message.status discriminados pelo campo status. Revisões anteriores
desta documentação listavam os dois como eventos separados — assinar aqueles
nomes falha na validação.
Payload de message.status
Acompanha os tiques de uma mensagem que você enviou — use para montar um indicador de entregue/lido ou uma visão de "aguardando resposta".
{
"message_id": "3EB0C9A17F2B4D8E1A05F3D77C10B4E2A991",
"to": "558585218491@s.whatsapp.net",
"status": "read",
"is_group": false,
"timestamp": 1779425257573
}| Campo | Notas |
|---|---|
status | Um de sent (chegou ao WhatsApp), delivered (chegou ao dispositivo, ✓✓), read (conversa aberta, ✓✓ azul), played (áudio/vídeo reproduzido), failed (não entregue). |
participant | Em grupos, o membro a quem este ack pertence — cada membro confirma de forma independente. Omitido em conversas 1:1. |
O mesmo message_id dispara várias vezes conforme o status avança. read só chega quando o destinatário tem as confirmações de leitura ativadas — sua ausência não prova que a mensagem está não lida.
Resolução de LID (Linked-Device ID)
O protocolo multi-device do WhatsApp expõe contatos a sessões linkadas como Linked-Device IDs (<digits>@lid) em vez de JIDs de telefone (<digits>@s.whatsapp.net). Respostas endereçadas a um JID @lid são silenciosamente descartadas no servidor do WhatsApp, então as resolvemos no lado do gateway antes de emitir o webhook.
Cada payload de message.received carrega três campos de rastreabilidade:
| Campo | Notas |
|---|---|
from | Melhor JID conhecido do remetente. <phone>@s.whatsapp.net quando resolvido, caso contrário o <digits>@lid bruto. |
from_resolved | true quando from é um JID de telefone entregável. false quando só o LID é conhecido até agora. |
from_lid | O JID @lid original, presente sempre que o WhatsApp entregou a mensagem como um LID — mesmo após a resolução. Permite correlacionar registros anteriores indexados por LID com o telefone resolvido. |
from_phone | O JID <phone>@s.whatsapp.net resolvido, presente quando from_resolved é true. Sempre igual a from nesse caso — exposto separadamente para você pegá-lo incondicionalmente sem checar o sufixo de from. |
Chave de persistência recomendada: from_phone quando presente, com fallback para from_lid. Quando from_resolved muda para true para um contato que você antes via apenas como LID, também emitimos um evento contact.resolved para você reescrever a chave de forma determinística.
Endereçamento de resposta: POST /v1/channels/:id/messages aceita
apenas JIDs de telefone. Qualquer to terminado em @lid é rejeitado de
cara com 400 undeliverable_recipient — conhecido ou não, em cache ou não. O
WhatsApp descarta em silêncio mensagens endereçadas a um LID, então recusamos em
vez de reportar uma entrega que nunca acontece.
Ou seja: quando from_resolved é false você não consegue responder ainda.
Guarde a mensagem sob from_lid, espere o contact.resolved, e então envie para
o JID de telefone.
Payload de contact.resolved
{
"channel_id": "8d653c66-e4ff-43ee-97da-3de5ad5680d4",
"lid": "47064251658474@lid",
"phone_jid": "558585218491@s.whatsapp.net",
"first_seen_at": "2026-05-22T21:17:11.576Z"
}Você também pode listar todos os pares (LID → JID de telefone) que observamos desde a abertura da sessão:
GET /v1/channels/{id}/contacts/lids
→ { "data": [{ "lid": "...@lid", "phone_jid": "...@s.whatsapp.net" }] }Headers que seu endpoint vai ver
POST /your/endpoint HTTP/1.1
Host: api.acme.dev
Content-Type: application/json
User-Agent: whatisup-webhooks/0.0.1
X-WhatIsUp-Signature: t=1700000000,v1=...
X-WhatIsUp-Event: message.received
X-WhatIsUp-Event-Id: 9bc38acc-2edc-482e-93d4-0482b432ffa6
X-WhatIsUp-Correlation-Id: req-3f7a1c02
X-WhatIsUp-Event-Id e X-WhatIsUp-Event são conveniências — espelham o que está no corpo, mas deixam você rotear ou curto-circuitar antes de fazer o parse do JSON.
X-WhatIsUp-Correlation-Id rastreia o evento de volta ao que quer que o tenha disparado. Para um message.sent de saída, é o id da requisição da chamada POST /v1/channels/{channel_id}/messages (o mesmo valor devolvido no header x-request-id). Para um message.received, é um ID gerado estável entre retentativas. Útil em logs.
Compatibilidade
Versionamos os schemas via api_version, hoje v1. Dentro de uma versão, vamos apenas adicionar campos opcionais — nunca renomear, nunca remover. Novos formatos incompatíveis ganham uma nova string de versão; você opta por eles atualizando seu endpoint no seu ritmo. (Ainda não temos múltiplas versões no ar — só uma.)
O stream SSE em /v1/events carrega os mesmos payloads data,
mas não é o mesmo envelope e não é o mesmo vocabulário de eventos —
frames SSE não são assinados, não vêm embrulhados e usam alguns nomes de evento
diferentes. Leia aquela página antes de escrever um handler só para os dois.