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úmero —
number.created,number.connected,number.disconnected,number.removedenumber.restoredsão assináveis em qualquer número, seja qual for o provedor. (Onumber.disconnectedsó dispara em número web nativo — vejanumber.disconnected.) - Eventos normalizados de chamada —
call.ringing,call.connected,call.ended,call.missedem qualquer número com voz habilitada. eventsnunca é opcional. Um webhook gravado comeventsvazio entrega nada, em qualquer vocabulário, e não avisa: é um201silencioso. 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: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).
Nomes de evento novos continuam surgindo — ignore os que você não conhece. Uma assinatura
"*" recebe um evento novo assim que ele passa a existir (o number.restored é um deles). Responda com 2xx a um evento que o seu código não trata e siga em frente. A partir do @pilot-status/sdk 0.12.0 (Node) e do pilot-status 1.10.0 (Python), parseCustomerWebhook e parse_customer_webhook deixam de lançar erro para um nome de evento que não conhecem: devolvem { type: "unknown", event, data, raw } (em Python, um dict com as mesmas chaves), para que um evento novo nunca quebre a sua integração. Um payload que não é um objeto com event em texto, ou um evento conhecido com campos inválidos, continua lançando erro. O trigger do n8n já repassa qualquer evento quando All Events está selecionado. Veja SDKs e n8n.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 mensagemmessage.received— mensagem recebida no número conectado
message.group— mensagem recebida em um grupomessage.newsletter— mensagem recebida em um canal (@newsletter)message.stories— Status (story) publicado por um contato
number.created— número criado no Pilot Statusnumber.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 removidonumber.restored— um número excluído foi restaurado dentro da janela de 30 dias; ele volta desconectado (vejanumber.restored)number.recovered— número recuperado de um estado degradado/bloqueado
number.health_blocked, number.health_degraded, number.health_shadowban —, number.recovered, number.inbound_stalled e number.inbound_recovered estão no seletor de eventos e são assináveis pelo nome em todos os provedores desde 15/09/2026; também chegam a webhooks assinados com o curinga "*". Como todo evento number.*, vão somente para os webhooks inscritos naquele número.
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/conectadacall.ended— chamada finalizada (status:COMPLETED|FAILED|REJECTED;durationem segundos quando atendida)call.missed— chamada de entrada encerrada sem ser atendidacall.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 nativocallsda Meta (brutoentry[].changes[].value, carregando SDPs) para sinalização WebRTC personalizada
flow.response_received— um cliente enviou um Flow, com as respostas dentro. Veja Payload deflow.*.
Identificadores e correlação
Todo evento de mensagem carrega dois IDs distintos:messageId— ID da mensagem do WhatsApp/provedor (ex.:key.id). Exceção: nummessage.failedenviado pelo worker ou pelo limite do plano ele traz o id interno, e o corpo plano do reconciliador não temmessageId— vejamessage.failedabaixo.id— ID interno da mensagem no Pilot Status. Mesmo valor doidnoHTTP 202dePOST /v1/messages/send.numberId— o ID público do número que tratou o evento. Em todo eventonumber.*ele agora é sempre o ID do número de WhatsApp (veja a mudança incompatível na famílianumber.*).correlationId— presente quando o evento se correlaciona a um envio anterior (mesmo valor docorrelationIddo202).quotedMessageId— emmessage.reply, omessageIdda mensagem original citada (igual aomessageIddomessage.sentoriginal).
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
⚠️ O
message.failed é a exceção: duas das três formas de corpo dele não seguem esta tabela — veja message.failed abaixo.
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; maisgroupId/groupNameounewsletterId/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
Omessage.failed é enviado só para números não oficiais (web) — um número oficial reporta a falha no statuses[] nativo da Meta. O corpo dele não é uniforme: depende de onde a falha foi detectada, e fica como está de propósito, porque quem já assina faz o parsing dele. Chegam três formas. Nenhum dos códigos delas é o errorCode da API: para classificar uma falha, leia o errorCode de GET /v1/messages/{messageId} — Códigos de erro do webhook vs da API liga um ao outro.
1. Forma de status — error e errorCode. Enviada quando o provedor reporta a mensagem como falha (errorCode: "SEND_FAILED") e quando o limite de mensagens do plano barra um envio (errorCode: "PLAN_LIMIT_REACHED"). Segue os campos comuns acima; o messageId é o id do provedor.
data.event, internalMessageId nem whatsappNumberId, e o messageId dela é o id interno, porque nada chegou ao provedor.
2. Forma do worker — errorMessage, sem código. Enviada pelo worker de envio nas falhas dele: destino fora do WhatsApp, shadowban, conexão ausente, desconectada ou removida, card de pedido que falhou depois de o texto sair, a última tentativa com falha, ou um erro inesperado. O data traz messageId, internalMessageId, correlationId, environment, destinationNumber, content, status, failedAt e o motivo em errorMessage — sem error, errorCode, id, from, to, type, fromMe nem createdAt. Aqui o messageId é o id interno, o mesmo valor de internalMessageId. As duas falhas de conexão (nenhuma conexão conectada, nenhuma atribuída) não trazem internalMessageId nem correlationId; o caminho de erro inesperado acrescenta sourceNumber.
code no nível de cima. O reconciliador de mensagens, que falha uma mensagem cujo prazo de entrega venceu, e a checagem desse prazo que o worker faz antes de uma retentativa mandam um corpo sem event e sem data: os campos ficam no nível de cima. Nenhum cabeçalho nomeia o evento, então reconheça esta forma por status: "FAILED" com internalMessageId no nível de cima. O reconciliador manda code: NUMBER_BLOCKED quando o número estava bloqueado, DELIVER_UNTIL_EXPIRED nos outros casos. A checagem da retentativa não manda code e acrescenta messageId (o id interno).
idempotencyKey de primeiro nível descrito em Deduplicar retentativas.
message.received
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á emcontent; 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.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.restored / number.recovered (e os eventos number.health_blocked / number.health_degraded / number.health_shadowban):
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 aos webhooks que o assinam pelo nome ou com"*".
error e errorCode vêm incluídos quando a transição de saúde carrega um motivo.
number.removed
number.restored
O par donumber.removed: um número excluído foi trazido de volta dentro da janela de 30 dias.
- Dispara em todo caminho de restauração: o painel, o
POST /v1/numbers/{id}/restoree a reconexão do mesmo telefone na Meta (Embedded Signup ou credenciais da Cloud API) enquanto o número excluído ainda pode ser restaurado. Essa reconexão traz o número antigo de volta, então onumber.restoredé seguido denumber.created/number.connected. - Restaura o registro, não a conexão: o número volta desconectado e precisa ser reconectado antes de poder enviar.
- Mesmo payload e mesmo escopo do
number.removed: o mesmonumberId(o id que oGET /v1/numbersdevolve), entregue só aos webhooks inscritos naquele número, inclusive assinaturas"*". Assinável pelo nome em qualquer provedor. - Se a sua integração deixa de aceitar um número ao receber
number.removed, volte a aceitá-lo ao recebernumber.restored. Caso contrário, você continua recusando os eventos dele depois da restauração.
Payload de call.*
Diferente de message.* / number.*, os eventos normalizados call.* são planos (sem o wrapper data):
Consulte o fluxo completo de chamadas.
Payload de flow.*
O flow.response_received dispara quando um cliente envia um Flow. Usa o envelope canônico { event, data } mesmo em número Meta, e está disponível apenas em números Meta Cloud API, porque Flows só existem lá.
Flow simples — sem endpoint, respostas no response
Flow data_exchange — respostas no submitted, anexo ainda em transferência
O
submitted é a lista das trocas data_exchange, da mais antiga para a mais nova. Cada item tem screen (uma string, ou null), at (ISO 8601), data (o objeto já parseado, ou null) e omitted. O campo é null — nunca [] — tanto quando não há nada para mostrar quanto quando o conteúdo foi ocultado, porque [] se leria como “houve respostas e nós as escondemos”.Só as trocas data_exchange são capturadas. Um INIT devolve o estado que nós semeamos no envio do Flow, e um BACK é navegação; nenhum dos dois é algo que a pessoa respondeu, então nenhum dos dois é guardado. A lista é, portanto, legitimamente mais curta que os traces do seu endpoint para a mesma sessão — um INIT que não aparece nela não é troca perdida.Ele captura a requisição que a Meta manda para nós, nunca o que o seu endpoint respondeu. O extension_message_response que a sua API devolve para encerrar o Flow continua não aparecendo em lado nenhum da Pilot Status, e este campo não muda isso — veja Conectar a minha API a um Flow.É append-only, e não há deduplicação. Quem volta uma tela e envia de novo gera um segundo item, genuíno, em vez de sobrescrever o primeiro; é o at que separa duas passagens sob o mesmo token.As trocas ficam guardadas por 30 dias + 1, e o dia a mais não é enfeite. Uma troca é capturada enquanto a pessoa ainda está preenchendo o formulário — minutos antes da submissão que cria a linha da resposta. Com 30 dias iguais dos dois lados, as trocas expirariam primeiro, e uma resposta ainda dentro da própria janela publicaria lista vazia nos seus últimos minutos.
Ou
response, ou responseRaw, nunca os dois. A Meta manda o corpo do nfm_reply como string JSON. Quando parseia, você recebe response; quando não parseia, recebe responseRaw com os bytes como chegaram. A submissão nunca é descartada por não parsear, então confira qual dos dois veio — e lembre que, num Flow data_exchange, o que está ali é o flow_token, não as respostas.Sem respostas nenhuma, não sai evento. Uma submissão que não carrega nada não dispara — um corpo vazio seria indistinguível, do seu lado, de um formulário enviado em branco.O evento também só dispara para uma submissão que foi gravada. Uma reentrega da Meta de algo já guardado é silenciosa, que é a deduplicação que você quer; e também é silenciosa uma segunda submissão, distinta, que colidiu no mesmo literal de
flow_token — o único caso em que o silêncio custa uma resposta. Tokens criados pelo Pilot Status nunca colidem; um Flow montado no Flow Manager da Meta com flow_token fixo pode.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 ementry[].changes[].field.
Envelope nativo (como é entregue)
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 emevents no POST /v1/webhooks de um número Meta. São também os nomes que chegam como evento entregue.
Mais as famílias que não são o envelope da Meta:
- Respostas de Flow —
flow.response_received, só em números Meta, no formato canônico{ event, data }. Veja Payload deflow.*. - Eventos normalizados de chamada —
call.ringing,call.connected,call.ended,call.missedecall.permission_updated(só Meta). Esses são eventos planos do Pilot Status, não o envelope da Meta — veja Payload decall.*. - Ciclo de vida do número —
number.created,number.connected,number.removed,number.restored. Onumber.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.Saúde do número — assinável desde 15/09/2026
Um número Meta nunca emitenumber.disconnected. O que ele emite é a família de saúde do número, e esses nomes estão agora no seletor de eventos — assine-os pelo nome, como qualquer outro:
Até 15/09/2026 eles só chegavam por
"*". O despachante sempre os casou, mas eles não apareciam no seletor — então um webhook assinado exatamente no que queria não recebia sinal de saúde nenhum, e nada dizia isso. Se você pegou "*" só por causa deles, já pode estreitar a assinatura.{ event, data } (data: numberId, phone, displayName, createdAt, e error / errorCode quando existirem) — não o envelope da Meta. Ou seja: uma assinatura que misture estes com campos da Meta entrega dois formatos de corpo na mesma URL: { object, entry } da Meta para os campos assinados, e { event, data } para estes mais call.*, flow.response_received e number.*. Ramifique pela presença de entry antes de fazer o parse.
Se você quiser o tráfego de conversa e o de saúde em endpoints diferentes, crie dois webhooks no número: um com os campos Meta que precisa, outro com
"*". Veja quantos webhooks por número.Ecos: o que o negócio enviou por fora
message_echoes e smb_message_echoes carregam uma cópia de mensagem enviada pelo negócio — o primeiro para envios feitos por qualquer outra ferramenta na mesma WABA, o segundo para envios feitos pelo app WhatsApp Business no celular (coexistência). Num número em coexistência, smb_message_echoes é como a resposta digitada pelo atendente no aparelho chega ao seu sistema.
A mensagem ecoada tem a mesma forma por tipo de uma mensagem recebida (text / image / video / audio / document / sticker / location / contacts / reaction), mais o destinatário:
statuses[] — é aqui que está a categoria cobrada
No campo messages, os status de entrega vêm em value.statuses, e cada um carrega o veredito de cobrança da própria Meta. Esta é a resposta autoritativa para “o que a Meta cobrou por esta mensagem” — não precisa ser inferida pelo template.
As combinações efetivamente observadas em produção (levantadas em 30 dias de entregas):
conversation.id e conversation.expiration_timestamp são também a forma honesta de responder “um segundo clique no anúncio abriu janela nova ou estendeu a antiga?” — compare os ids em vez de supor. O pricing.type então diz se a Meta cobrou.Ligar um status ao seu envio — biz_opaque_callback_data
Uma mensagem enviada de um número oficial com POST /v1/messages/send vai para a Meta com o id do HTTP 202 em biz_opaque_callback_data, e a Meta o devolve em cada item de statuses[] dessa mensagem. Isso liga o seu pedido ao status da Meta diretamente: sem consulta e sem esperar o wamid.
- É o
id, nunca ocorrelationId. OcorrelationIdé um valor seu e não precisa ser único. Oidé único. - Observado em
sent,deliverederead, tanto em template quanto em texto livre. - Só em mensagens enviadas pela Pilot Status a partir de 17/09/2026, 14h23 (horário de Brasília). A chave fica ausente, e não vazia, nos status de mensagens enviadas antes dessa data, direto pela Graph API ou pelo app WhatsApp Business (veja ecos).
- Se o campo faltar, use o wamid.
GET /v1/messages/{messageId}aceita o wamid (statuses[].id) e devolve oide ocorrelationIdda mensagem.
Referral de CTWA — onde cai o clique no anúncio
Não existe um camporeferral separado para assinar. O bloco chega dentro do envelope de messages, em entry[0].changes[0].value.messages[0].referral, verbatim.
source_idé o id do anúncio (source_type: "ad") ou do post (source_type: "post"). Trate-o como chave opaca de agrupamento: resolvê-lo para nome de campanha/anúncio pela Graph exigeads_readna conta de anúncios do anunciante, o que um token de system user do WhatsApp não carrega (voltaGraphMethodExceptioncode 100 / subcode 33).- Não existe UTM num referral da Meta. Se você precisa dos nomes de campanha, use
GET /v1/referrals— devolve a linha na hora, com os nomes resolvidos de forma assíncrona (namesResolvedAt: nullsignifica “ainda não resolvido, pergunte de novo”).
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.
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),mediaCaptiono texto que acompanha,mediaFilenameo 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 aPOST /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
contentpodem 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.