Catálogo de eventos
Todos os eventos que o hub envia ao seu webhook, com o formato de cada um.
O campo type do envelope assume um destes valores. Os connection.* e whatsapp.* são emitidos; os de lead ainda não.
type |
Quando | Situação |
|---|---|---|
connection.created |
Cliente concluiu a conexão do canal | emitido |
connection.updated |
Dados ou estado da conexão mudaram | emitido |
connection.disconnected |
Conexão encerrada ou revogada | emitido |
connection.token_expiring |
Credencial da conexão perto de expirar | emitido |
whatsapp.message.received |
Mensagem recebida | emitido |
whatsapp.message.status |
Mudança de estado de um envio no hub, ou status de entrega da Meta | emitido |
whatsapp.message.echo |
Mensagem enviada pelo app do WhatsApp Business (Coexistence) | emitido |
whatsapp.history.received |
Bloco do histórico sincronizado (Coexistence) | emitido |
whatsapp.contacts.synced |
Contatos do app WhatsApp Business sincronizados (Coexistence) | emitido |
whatsapp.identity.updated |
O contato trocou de número ou de BSUID | emitido |
whatsapp.media.updated |
Download de uma mídia recebida terminou (ou falhou) | emitido |
whatsapp.template.status_updated |
Status de aprovação de template mudou | emitido |
whatsapp.template.updated |
Categoria, qualidade ou componentes de template mudaram | emitido |
whatsapp.usage.free_tier |
O nível gratuito de mensagens de serviço do número chegou a 800 ou acabou | emitido |
leadgen.lead.created |
Novo lead de formulário do Lead Ads | planejado |
Trate type desconhecido como "ignorar e responder 2xx", para novos eventos não gerarem retries no seu receptor. Vale o mesmo para campo novo dentro de data e para message.type desconhecido.
Eventos de conexão
Em todos, connectionId do envelope é o id da conexão e data é a conexão no mesmo formato de GET /v1/connections/:connectionId (sem o envoltório connection), no estado logo após a mudança. Use createdAt do envelope para ordenar, já que a ordem de chegada não é garantida.
type |
Origem | meta |
|---|---|---|
connection.created |
Cliente concluiu o fluxo; status ainda pending |
null |
connection.updated |
Ativação concluída (pending para active), falha de ativação (error), reconexão, token vencido (error com token_expired), aviso da Meta ou mudança do health (meta.health: "changed") |
ver abaixo |
connection.disconnected |
DELETE /v1/connections/:id, ou a Meta removeu o acesso |
{ "reason": ..., "accountEvent": ... } |
connection.token_expiring |
A credencial vence em até 7 dias; um aviso por conexão | null |
connection.disconnected. meta.reason diz quem encerrou: api (seu DELETE), meta_uninstalled (o cliente removeu o parceiro no WhatsApp Manager) ou meta_account_deleted (a conta do WhatsApp foi apagada). Nos dois últimos, meta.accountEvent traz o nome do evento da Meta (PARTNER_APP_UNINSTALLED, ACCOUNT_DELETED); no primeiro o campo não existe.
Eventos originados por avisos da Meta. Quando um connection.updated nasce de um aviso de conta da Meta, o envelope traz meta: { "accountEvent": "<nome>" }:
meta.accountEvent |
O que mudou na conexão |
|---|---|
ACCOUNT_OFFBOARDED |
active passa para error com errorCode: "offboarded" |
ACCOUNT_RECONNECTED |
error com offboarded volta para active |
DISABLED_UPDATE, ACCOUNT_RESTRICTION, ACCOUNT_VIOLATION |
Nada muda no estado: é um aviso de que a conta sofreu restrição ou violação. Sai um connection.updated para cada conexão active, pending ou error da conta (WABA), e sai de novo se a Meta reentregar o mesmo aviso: deduplique pelo seu lado se a reação não puder repetir |
MM_LITE_TERMS_SIGNED |
Os termos do Marketing Messages API foram aceitos; emitido uma vez por conta |
Os demais avisos de conta da Meta não geram evento. Se o seu receptor precisa reagir a uma restrição, trate connection.updated com meta.accountEvent em DISABLED_UPDATE, ACCOUNT_RESTRICTION ou ACCOUNT_VIOLATION.
connection.token_expiring. Chega uma vez, até 7 dias antes de tokenExpiresAt. Se nada for feito, no vencimento a conexão vai para error com token_expired (connection.updated) e o cliente precisa repetir o fluxo de conexão com uma sessão nova.
Eventos do WhatsApp
Em todos, connectionId do envelope é a conexão do número, exceto nos eventos de template, que são da conta (WABA) e vêm com connectionId: null. Datas em ISO 8601. meta, quando preenchido, traz o item original da Meta, só para diagnóstico: o contrato é o data.
O hub não entrega o que chegou antes da conexão atual. Mensagem, status ou aviso com data anterior à conexão do número ao workspace (ou à entrada da conta no workspace) é descartado: é dado do dono anterior. Isso vale também para retries da Meta depois de desconectar e reconectar em outro workspace.
whatsapp.message.received
{
"phoneNumberId": "109876543210987",
"message": {
"wamid": "wamid.HBgM...",
"type": "text",
"timestamp": "2026-10-23T15:00:00.000Z",
"from": { "userId": "BR.1349...", "parentUserId": null, "waId": "5543999990000", "username": null, "name": "Maria" },
"groupId": null,
"context": null,
"referral": null,
"text": { "body": "Oi" }
},
"window": { "expiresAt": "2026-10-24T15:00:00.000Z" }
}
meta:{ message: <item original>, contact: <contacts[] do remetente> | null }.- O conteúdo fica num campo com o nome do
type:text,media(paraimage,audio,video,document,sticker:{ id, status, mimeType, sha256, caption, filename, voice, animated }, comiddo hub),location,contacts,interactive(button_reply,list_reply,nfm_replycomflow.responsejá como objeto,product),button(resposta de botão de template),reaction(emoji: null= reação removida),order,revoke,edit({ wamid, message }, com a mensagem nova no mesmo formato;editerevokesó chegam de número Coexistence, porque a Meta só oferece esses webhooks para o app WhatsApp Business, e o contato só vê "Editar" e "Apagar para todos" nessas conversas) eunsupported({ type, errors: [{ code, title }] }). interactive.product.priceAmount/salePriceAmountvêm como a Meta manda: o valor multiplicado por 1 000.order.productItems[].itemPricevem como número decimal.typefora da lista conhecida mantém o nome e ganhaunsupported; ignore o que não reconhecer e responda 2xx.context:{ wamid, from, forwarded, frequentlyForwarded, referredProduct }quando a mensagem responde ou cita outra;referralem mensagem vinda de anúncio (comctwaClid).groupIdvem preenchido em mensagem de grupo; ela não abre janela (window: null).- Deduplicação: o hub emite uma vez por
wamid, mas a entrega é "pelo menos uma vez"; deduplique peloiddo envelope.
whatsapp.message.status
Dois tipos de origem, pelo data.source:
source: "hub" (transição do envio no hub: accepted, failed, uncertain, abandoned):
{
"messageId": "...",
"wamid": "wamid.HBgM...",
"source": "hub",
"status": "accepted",
"timestamp": "2026-10-23T15:00:00.000Z",
"pacing": null,
"error": null
}
error é { code, metaCode } em failed; em uncertain vem null, e o código da Meta, quando houver, aparece só em metaErrorCode de GET …/messages/:messageId. meta traz { reconciled: true } (e overridesAdminResolution: true) quando o accepted veio da conciliação, e { resolvedByAdmin: true } no abandoned. O accepted com overridesAdminResolution vence o abandoned do mesmo messageId em qualquer ordem.
source: "meta" (status de entrega do webhook da Meta: sent, delivered, read, played, failed):
{
"messageId": "...",
"wamid": "wamid.HBgM...",
"source": "meta",
"status": "delivered",
"timestamp": "2026-10-23T15:00:03.000Z",
"recipient": { "userId": "BR.1349...", "parentUserId": null, "waId": "5543999990000" },
"errors": [],
"pricing": { "billable": true, "model": "PMP", "type": "regular", "category": "utility", "subtype": null },
"conversation": null
}
- Correlação: use
messageId(id do envio no hub). Ele vemnullquando o status chegou antes da resposta da Meta ao envio, ou é de mensagem enviada fora do hub; nesse caso correlacione pelowamid, que chega noaccepted(source: "hub"). O status commessageId: nullnão é reemitido quando o hub liga a mensagem depois; odeliveryStatedo envio já reflete o status. pricingvem só em alguns status (sente um dedelivered/read), comcategorycomo a Meta manda (marketing_lite,authentication-international…).conversationquase semprenull: desde a Graph v24.0 a Meta só o manda em mensagem enviada dentro de uma janela de ponto de entrada gratuito, eexpiresAtsó vem nosent. Reação enviada pelo hub chegou no teste manual só comsente sempricing. Ver Custos.errors:[{ code, title, details }]emfailed.131049(esperar),132015(template pausado) e131050(contato não quer marketing) são só repassados: a decisão é do seu app.- Cada status (por mensagem e por valor de
status) sai uma vez. A Meta pode pulardeliveredquando entrega e leitura chegam juntas.
whatsapp.message.echo
Mensagem que o próprio negócio enviou pelo app WhatsApp Business (Coexistence): { phoneNumberId, message }, com message no formato do recebido e message.from sendo o negócio, mais message.to (identidade do contato). meta: { message, contact }. Eco não abre janela.
whatsapp.history.received
Histórico de até 180 dias do app WhatsApp Business (Coexistence), em blocos de até 100 mensagens de uma conversa:
{
"phoneNumberId": "109876543210987",
"phase": 0,
"chunkOrder": 3,
"progress": 40,
"thread": { "userId": "BR.1349...", "parentUserId": null, "waId": "5543999990000", "username": null },
"messages": []
}
- Cada mensagem tem o formato do recebido mais
direction(inboundououtbound) ehistoryStatus(status da Meta em maiúsculas). Mídia antiga vem comotypemedia_placeholder. - Os blocos chegam fora de ordem; ordene pelo
timestampdas mensagens.progress100 marca o fim (também visível emsync[].progressda conexão). - A mídia dos últimos 14 dias chega depois, num evento próprio por mensagem, com
phase,chunkOrdereprogressnull, e a mídia baixada como em qualquer mensagem recebida. - Histórico não abre janela.
whatsapp.contacts.synced
Contatos do app WhatsApp Business (Coexistence): { phoneNumberId, contacts: [{ action, userId, parentUserId, username, phone, fullName, firstName, timestamp }] }, com action add ou remove. O hub não guarda contatos.
whatsapp.identity.updated
{
"kind": "user_changed_user_id",
"previous": { "userId": "BR.1349...", "parentUserId": null },
"current": { "userId": "BR.8812...", "parentUserId": null, "waId": "5543999990000" },
"timestamp": "2026-10-23T15:00:00.000Z"
}
kind é user_changed_number (trocou de telefone) ou user_changed_user_id (trocou de BSUID).
whatsapp.media.updated
{ id, status, mimeType, size, sha256, wamid, errorCode }, com status stored (pronta: GET /v1/media/:id), failed ou expired. Pode chegar antes do whatsapp.message.received da mesma mensagem.
whatsapp.template.status_updated
{ template, event, reason, otherInfo: { title, description } | null, disableDate }, com template no formato da API de templates e event como a Meta manda (APPROVED, REJECTED, PAUSED, DISABLED, PENDING_DELETION, FLAGGED, REINSTATED…). Um webhook mais velho que o status gravado não gera evento.
whatsapp.template.updated
{ template, change }, com change category_pending (a Meta avisou que vai trocar a categoria: template.correctCategory e pendingCategoryAt), category (troca concluída), quality (template.qualityScore) ou components (o template foi editado; template.components já traz a versão nova). Aviso de qualidade ou de categoria mais velho que o último aplicado (a Meta reentrega fora de ordem) não muda o template nem gera evento.
whatsapp.usage.free_tier
Nível gratuito de mensagens de serviço do número: { phoneNumberId, month, timezone, level, limit, free, charged }, com level warning (800 gratuitas no mês) ou exhausted (primeira cobrada). Um por nível, número e mês. Detalhes em Custos.