Skip to main content

Referência de eventos de webhook

Webhooks entregam eventos em tempo real para a URL do seu sistema. Esta página documenta cada evento, seu payload e como correlacionar eventos com suas chamadas de API. Para criar e gerenciar webhooks, consulte Configurar webhooks.

Quais eventos o seu número envia

O vocabulário de eventos depende de como o número foi conectado. Um número pareado por QR code fala o vocabulário canônico do Pilot Status, documentado nesta página; um número da Meta Cloud API encaminha o envelope nativo da própria Meta. São formatos diferentes, com nomes diferentes, e nenhuma configuração de assinatura converte um no outro. Três coisas são iguais em todos os vocabulários:
  • Ciclo de vida do númeronumber.created, number.connected, number.disconnected e number.removed são assináveis em qualquer número, seja qual for o provedor. (O number.disconnecteddispara em número web nativo — veja number.disconnected.)
  • Eventos normalizados de chamadacall.ringing, call.connected, call.ended, call.missed em qualquer número com voz habilitada.
  • events nunca é opcional. Um webhook gravado com events vazio entrega nada, em qualquer vocabulário, e não avisa: é um 201 silencioso. Assine os nomes que você quer, ou o curinga "*".
Não sabe de que tipo é o seu número? O GET /v1/numbers devolve o provider de cada um: PILOT_STATUS para web nativo, META para Cloud API. Veja Oficial vs. não oficial.

Formato do payload

Todo evento chega como JSON:
Esquema v3 (camelCase). Todos os campos de data são camelCase. Números de telefone são sempre E.164 com + (sem sufixo de dispositivo, sem @s.whatsapp.net / @lid). Timestamps são sempre ISO 8601 no campo único createdAt (quando a mensagem aconteceu, segundo o provedor — não o horário em que a processamos).
Mensagem antiga não é replicada. Quando um número conecta, o WhatsApp entrega ao provedor o histórico do aparelho. Essas mensagens são importadas para o chat, mas não são entregues no seu webhook — senão cada reconexão re-entregaria até 30 dias de conversa e nada no payload permitiria distinguir isso do tráfego que acabou de chegar.Para receber mesmo assim, ligue settings.webhookHistoricalMessages por número em PATCH /v1/numbers/{id}. Para LER o histórico, use GET /v1/messages/history.

Eventos disponíveis

Saída (entrega/status): message.sent, message.delivered, message.read, message.failed Entrada (mensagens recebidas):
  • message.reply — resposta que cita outra mensagem
  • message.received — mensagem recebida no número conectado
message.reply é exclusivo — ele NÃO dispara também o message.received.Quando um contato responde citando uma mensagem anterior (deslizando para responder no WhatsApp), o evento é message.reply e somente ele. Se o seu webhook assina message.received e não assina message.reply, essas mensagens nunca chegam até você — e nada acusa erro, então parece que o cliente não escreveu.Assine os dois, ou use o curinga *, se quiser toda mensagem de entrada.
  • message.group — mensagem recebida em um grupo
  • message.newsletter — mensagem recebida em um canal (@newsletter)
  • message.stories — Status (story) publicado por um contato
Ciclo de vida do número:
  • number.created — número criado no Pilot Status
  • number.connected — conectado ao WhatsApp (estado OPEN)
  • number.disconnected — queda de conexão confirmada pela transição de saúde do número, em números web nativos (não oficiais) (um evento por queda confirmada, não um por oscilação de conectividade).
  • number.removed — número removido
  • number.recovered — número recuperado de um estado degradado/bloqueado (entregue somente a assinaturas com o curinga "*")
Eventos de saúde (somente assinatura "*"): os eventos de transição de saúde — number.health_blocked, number.health_degraded, number.health_shadowban — e number.recovered não são assináveis individualmente e não aparecem no seletor de eventos. Eles são entregues somente a webhooks assinados com o curinga "*". Chamadas de voz (WhatsApp Business Calling, consulte Chamadas de Voz): Os eventos de chamada normalizados — call.ringing, call.connected, call.ended, call.missed — estão disponíveis em qualquer número com suporte a chamadas (web nativo (não oficial) — e Meta Cloud API). call.permission_updated e o envelope nativo calls são somente da Meta Cloud API.
  • call.ringing — chamada tocando (UIC de entrada ou transição BIC de saída)
  • call.connected — chamada atendida/conectada
  • call.ended — chamada finalizada (status: COMPLETED | FAILED | REJECTED; duration em segundos quando atendida)
  • call.missed — chamada de entrada encerrada sem ser atendida
  • call.permission_updated — (somente Meta Cloud API) resposta a uma solicitação de permissão de chamada (status: NO_PERMISSION | TEMPORARY | PERMANENT)
  • calls — (somente Meta Cloud API) envelope nativo calls da Meta (bruto entry[].changes[].value, carregando SDPs) para sinalização WebRTC personalizada

Identificadores e correlação

Todo evento de mensagem carrega dois IDs distintos:
  • messageId — ID da mensagem do WhatsApp/provedor (ex.: key.id). Pode ser null para falhas que ocorrem antes de o provedor retornar um ID.
  • id — ID interno da mensagem no Pilot Status. Mesmo valor do id no HTTP 202 de POST /v1/messages/send.
  • numberId — o ID público do número que tratou o evento. Em todo evento number.* ele agora é sempre o ID do número de WhatsApp (veja a mudança incompatível na família number.*).
  • correlationId — presente quando o evento se correlaciona a um envio anterior (mesmo valor do correlationId do 202).
  • quotedMessageId — em message.reply, o messageId da mensagem original citada (igual ao messageId do message.sent original).

Correlação com POST /v1/messages/send

Após um envio aceito, a API retorna HTTP 202 com id e correlationId: Em message.reply: quotedMessageId = o messageId do message.sent original; o próprio messageId da resposta é a nova mensagem de entrada. Use quotedMessageId (e correlationId quando presente) para associar a resposta ao seu envio anterior.

Payload de message.*

Campos comuns

Semântica de direção de from/to:
  • Saída (sent/delivered/read/failed): to = número de destino (E.164); from = número próprio (presente quando resolvível de forma barata, caso contrário omitido); fromMe = true.
  • Entrada (received/reply): from = contato/remetente (E.164); to = número próprio (E.164); fromMe = false.
  • Grupo / canal (group/newsletter): from = participante (E.164); to = número próprio; mais groupId/groupName ou newsletterId/newsletterName.

message.sent

message.delivered

message.read

message.read só dispara quando o destinatário tem os recibos de leitura do WhatsApp ativados.

message.failed

Inclui error e, quando disponível, um errorCode estável (ex.: DELIVER_NOT_CONFIRMED).

message.received

Exemplo de mídia. Em eventos de entrada (message.received / message.reply) com mídia, mediaLink é uma URL pública e durável para a mídia re-hospedada no armazenamento do Pilot Status — para todos os provedores (Evolution GO, Evolution v2 e Meta Cloud API), incluindo mídia criptografada de ponta a ponta do WhatsApp, que é descriptografada no servidor antes de re-hospedar. Os campos media* são omitidos em mensagens somente-texto.

message.reply

O novo texto do contato está em content; o texto da mensagem original citada está em contentReplied. Use quotedMessageId para corresponder ao messageId do seu message.sent de saída original.

message.group

message.newsletter

message.stories

Status (stories) do WhatsApp publicados por um contato que o telefone conectado enxerga.
Só é entregue enquanto alguém assina. O tráfego de Status vem desligado no provider por padrão (ignoreStatus); assinar um webhook em message.stories — ou no curinga "*" — é o que o liga para aquele número. Ao remover a última assinatura, o provider volta a não enviar Status.
Status não é persistido. Não cria conversa nem mensagem no seu histórico, e não aparece em GET /v1/messages. O WhatsApp expira stories em 24h; o mediaLink que re-hospedamos é durável, o story não.

Payload de number.*

Campos para number.created / number.connected / number.disconnected / number.removed / number.recovered (e os eventos number.health_blocked / number.health_degraded / number.health_shadowban):
Mudança incompatível — data.numberId é sempre o ID do número. Em todo evento number.*, data.numberId agora é o ID do número de WhatsApp. Antes, number.created, number.connected e number.removed carregavam o ID interno da instância, enquanto os eventos de saúde já carregavam o ID do número: dois IDs opacos no mesmo campo, de modo que cruzar as duas famílias nunca casava — em silêncio. Se o seu consumidor casava os eventos de ciclo de vida pelo ID da instância, ele vai parar de casar — passe a usar o ID do número. É o mesmo id que o GET /v1/numbers devolve.

number.created

number.connected

number.disconnected

Emitido a partir da transição de saúde do número, que é quem controla a janela anti-oscilação e a deduplicação de um alerta por transição: você recebe um evento por queda confirmada, não um por oscilação do socket.
  • Números web nativos (não oficiais): o evento dispara em uma queda de conexão confirmada. Antes, um número não oficial que caía não produzia nenhum evento assinável — apenas os eventos de saúde, exclusivos do curinga "*".
  • Números da Meta Cloud API: nunca emitem number.disconnected. Uma transição de qualidade/status da Meta emite apenas o evento de saúde correspondente (number.health_blocked / number.health_degraded / number.health_shadowban), que chega às assinaturas "*".
error e errorCode vêm incluídos quando a transição de saúde carrega um motivo.

number.removed

Payload de call.*

Diferente de message.* / number.*, os eventos normalizados call.* são planos (sem o wrapper data):
Consulte o fluxo completo de chamadas.

Eventos da Meta Cloud API (envelope nativo)

Um número conectado pela Meta Cloud API — Embedded Signup ou credenciais coladas — não envia os eventos canônicos acima. Ele encaminha o próprio envelope de webhook da Meta, literal, com a chave no nome do campo que a Meta põe em entry[].changes[].field.
message.received nunca dispara em número Meta, e events: ["*"] não converte o formato. O nome do evento que você assina é o nome do campo da Meta — messages — e o corpo que chega é o envelope da Meta. Um receptor escrito para o esquema canônico ({ event, data }) lê undefined num payload Meta e não reporta nada, o que parece exatamente com “o cliente nunca escreveu”. Escreva o parser para o formato nativo, ou ramifique pelo provider do número.
Envelope nativo (como é entregue)
O entry[].changes[].value é o payload da Meta para aquele campo, sem modificação — o mesmo formato que a referência da própria Meta documenta. Nada é acrescentado e nada é removido, então um receptor já escrito para os webhooks da Meta continua funcionando. Uma mudança por entrega. A Meta pode agrupar várias mudanças num mesmo entry; cada uma é casada com as assinaturas e entregue sozinha, então entry e entry[0].changes têm sempre exatamente um elemento. Mantenha os laços que o seu parser da Meta já tem — eles simplesmente sempre rodarão uma vez — e nunca assuma que duas mudanças na mesma requisição têm relação entre si.

Campos assináveis

Estes são os nomes de campo que você pode colocar em events no POST /v1/webhooks de um número Meta. São também os nomes que chegam como evento entregue. Mais as duas famílias compartilhadas com todos os outros vocabulários:
  • Eventos normalizados de chamadacall.ringing, call.connected, call.ended, call.missed e call.permission_updated (só Meta). Esses são eventos planos do Pilot Status, não o envelope da Meta — veja Payload de call.*.
  • Ciclo de vida do númeronumber.created, number.connected, number.removed. O number.disconnected é assinável mas nunca dispara em número Meta: uma transição de qualidade/status da Meta emite um evento de saúde do número, e esses só chegam em assinaturas "*".
Nove campos da Meta nunca são entregues, nem para uma assinatura "*": account_alerts, automatic_events, history, messaging_handovers, partner_solutions, security, smb_app_state_sync, standby e tracking_events. São internos (backfill de histórico em massa, sincronia de estado da coexistência) ou tráfego administrativo/de segurança, e também não aparecem no seletor de eventos. Todo o resto que a Meta manda de um campo assinado é encaminhado.

Eventos da camada de migração (Evolution GO e v2)

Um número que você trouxe por uma camada de compatibilidade mantém o vocabulário de eventos do próprio provedor — o objetivo da camada é justamente os seus handlers não mudarem:
  • Evolution GO — PascalCase nativo (Message, SendMessage, Receipt, Presence, …), com os apelidos UPPER_SNAKE também aceitos. Lista completa na página da camada Evolution GO.
  • Evolution v2 — nomes nativos com ponto (messages.upsert, messages.update, connection.update, …). Lista completa na página da camada Evolution V2.
Os dois também carregam as famílias normalizadas call.* e number.* descritas acima.
Eventos de grupo e canal nesses dois exigem plano pago: groups.upsert, groups.update, group-participants.update (v2) e GroupInfo, JoinedGroup, NewsletterJoin, NewsletterLeave (GO) ficam escondidos do seletor de eventos numa conta gratuita. Os canônicos message.group e message.newsletter não têm essa restrição.

Notas importantes

  • message.stories: from é o contato que publicou o story. Não há to — um Status é difundido para a lista de contatos de quem publica, não endereçado ao seu número.
  • message.newsletter: newsletterName é o nome de exibição do canal quando disponível; caso contrário, pode ser omitido. O identificador do canal é newsletterId (JID completo ...@newsletter). participantName é o autor da mensagem no canal.
  • Mídia: em eventos de entrada com mídia, mediaLink é uma URL pública e durável para a mídia re-hospedada — para todos os provedores (Evolution GO, Evolution v2, Meta Cloud API), incluindo mídia criptografada de ponta a ponta descriptografada no servidor. mediaType é a categoria (image/video/audio/document/sticker), mediaCaption o texto que acompanha, mediaFilename o nome do arquivo original (documentos). Os quatro são omitidos em mensagens somente-texto e quando a redação de PII do número apaga o conteúdo.
  • Eventos number.* não são correlacionados a POST /v1/messages/send; use-os para provisionamento/monitoramento. Em todos eles, data.numberId é sempre o ID do número de WhatsApp — veja a mudança incompatível.
  • number.disconnected é emitido a partir da transição de saúde do número (um evento por queda confirmada, não um por oscilação de conexão), e somente para números web nativos (não oficiais); números da Meta Cloud API sinalizam problemas pelos eventos de saúde.
  • A entrega de campos sensíveis pode depender da configuração de retenção. Com a retenção desligada, campos condicionais como content podem ficar vazios; IDs e timestamps continuam existindo.
  • O evento message.read (e o status Read na API/Logs) só ocorre quando o destinatário tem os recibos de leitura do WhatsApp ativados.