Pular para o conteúdo principal

Catálogo de eventos

curl https://api.joinotify.com/webhook-endpoints/events \
-H 'Authorization: Bearer sk_live_xxx'

Um endpoint com events: [] recebe todos.

Mensagens​

EventoQuando dispara
messagesMensagem recebida, ou mudança de status de uma que você enviou
smb_message_echoesMensagem enviada pelo aplicativo do celular (coexistência)
historyBlocos do histórico sincronizado
smb_app_state_syncContatos adicionados, renomeados ou removidos no celular

messages é o evento que carrega mais coisa. O value pode trazer messages[] (o cliente escreveu) ou statuses[] (algo mudou no que você enviou).

Mensagem recebida
{
"field": "messages",
"value": {
"metadata": { "phone_number_id": "106540352242922" },
"contacts": [{ "profile": { "name": "Maria" }, "wa_id": "5541999999999" }],
"messages": [
{
"from": "5541999999999",
"id": "wamid.HBgN...",
"timestamp": "1785312000",
"type": "text",
"text": { "body": "Oi, chegou meu pedido?" }
}
]
}
}
Status de entrega
{
"field": "messages",
"value": {
"statuses": [
{
"id": "wamid.HBgN...",
"status": "delivered",
"timestamp": "1785312042",
"recipient_id": "5541999999999",
"conversation": { "id": "...", "origin": { "type": "service" } },
"pricing": { "billable": true, "category": "service" }
}
]
}
}
statusSignificado
sentSaiu da infraestrutura da Meta
deliveredChegou ao aparelho
readFoi aberta, se o cliente não desativou a confirmação
failedNão foi entregue — o motivo vem em errors[]

O objeto pricing diz se aquela conversa é cobrada e em que categoria. É a fonte certa para conciliar custo.

Templates​

EventoQuando dispara
message_template_status_updateAprovado, rejeitado, pausado ou desabilitado
message_template_quality_updateQualidade mudou (GREEN / YELLOW / RED)
message_template_components_updateConteúdo do template foi alterado
template_category_updateA Meta reclassificou a categoria
{
"field": "message_template_status_update",
"value": {
"event": "APPROVED",
"message_template_id": 1234567890,
"message_template_name": "confirmacao_pedido",
"message_template_language": "pt_BR"
}
}

Assine message_template_status_update em vez de consultar GET /templates em loop — é por ele que a aprovação chega.

template_category_update merece atenção: uma reclassificação de UTILITY para MARKETING aumenta o custo de cada envio daquele template, sem aviso prévio.

Conta​

EventoQuando dispara
account_alertsAlerta da Meta sobre a conta
account_updateMudança no estado da conta empresarial
account_review_updateResultado da revisão da conta
account_settings_updateConfigurações alteradas

account_update inclui banimentos e restrições. Vale monitorar: é o aviso de que a operação vai parar.

Número​

EventoQuando dispara
phone_number_name_updateResultado da solicitação de nome de exibição
phone_number_quality_updateQualidade do número mudou
{
"field": "phone_number_quality_update",
"value": {
"display_phone_number": "5541987111527",
"event": "FLAGGED",
"current_limit": "TIER_1K"
}
}

FLAGGED significa que a qualidade caiu para RED e o limite de mensagens pode ser reduzido. É o momento de rever o que está sendo disparado, não de disparar mais.

Negócio​

EventoQuando dispara
business_capability_updateLimites e capacidades mudaram
business_status_updateStatus de verificação do negócio mudou

Outros​

EventoQuando dispara
securityEventos de segurança da conta
flowsInterações com WhatsApp Flows
callsEventos de chamada
user_preferencesCliente optou por sair ou voltou a aceitar mensagens

user_preferences é o opt-out. Ignorá-lo é caminho curto para a qualidade do número despencar — e, em muitos casos, um problema de conformidade.

Cobrança​

Eventos que nós originamos, como os de carteira e engajamento mais abaixo — os de cima vêm todos da Meta e são repassados. Por isso o nome tem prefixo, e o payload traz source: "joinotify".

EventoQuando dispara
billing.past_dueO pagamento falhou e a tolerância começou. Ainda envia
billing.blockedAcesso cortado: as rotas de envio passam a responder 402
billing.restoredVoltou a poder enviar

O gatilho é o direito de uso, não o status do meio de pagamento. A diferença importa: você pode parar de enviar porque o cartão falhou, porque a licença venceu, porque o teste acabou ou porque estourou o teto de mensagens do plano. Para quem integra, os quatro são o mesmo fato — e reason diz qual foi.

billing.blocked
{
"field": "billing.blocked",
"value": {
"state": "blocked",
"reason": "license_expired",
"message": "The license for this account has expired. Renew it to keep sending messages.",
"previous_state": "ok",
"billable_numbers": 2,
"grace_ends_at": null,
"source": "joinotify",
"occurred_at": "2026-08-15T21:00:00.000Z"
}
}

O evento sai na transição, nunca repetido: um bloqueio que dura um mês gera um billing.blocked, não um por hora.

São eventos de conta

Saem sem wabaId e sem phoneNumberId — cobrança é da conta, não de um número. A consequência é que um endpoint filtrado por número não os recebe. Para receber cobrança, registre um endpoint sem filtro.

Carteira e indicações​

Se a conta participa do programa de indicação, cinco eventos a mais descrevem o dinheiro que entra e sai da carteira. Também são de conta, também levam source: "joinotify".

EventoQuando dispara
wallet.creditedEntrou saldo: comissão, ajuste do operador ou devolução de saque falho
wallet.debitedSaiu saldo: aplicado numa fatura, ou reservado por um pedido de saque
affiliate.approvedA inscrição no programa foi aprovada e o código passou a valer
affiliate.rejectedA inscrição foi recusada ou suspensa — reason diz por quê
payout.paidO saque foi pago, com method (pix/paypal) e payment_ref
wallet.credited
{
"field": "wallet.credited",
"value": {
"amount_cents": 2000,
"currency": "brl",
"bucket": "pending",
"available_at": "2026-09-18T12:00:00.000Z",
"reason": "commission",
"referral_id": "ref_1a2b3c",
"source": "joinotify",
"occurred_at": "2026-08-19T12:00:00.000Z"
}
}

payout.paid leva também paid_amount_cents, paid_currency e fee_cents quando o saque saiu por PayPal — lá o valor que chega não é o que saiu, porque a taxa e a conversão acontecem do outro lado. Num Pix os três vêm nulos.

bucket é o que evita a confusão mais cara desse fluxo: a comissão nasce retida (pending) e só vira saldo sacável (available) quando a retenção vence — é o que absorve um reembolso que chega depois. Um wallet.credited com bucket: "pending" não é dinheiro disponível ainda, e available_at diz quando será.

Engajamento​

Nove eventos contam o que aconteceu aqui dentro com os seus contatos, campanhas, fluxos e conversas. Também levam source: "joinotify", e são o que permite reagir sem consultar a API em loop — "o fluxo capturou um lead → criar o cliente no ERP".

EventoQuando dispara
contact.createdUm contato novo: primeira mensagem, API, fluxo ou painel. Importações não anunciam linha a linha
contact.opted_inConsentimento de marketing registrado — pelo painel, pela API ou por um fluxo
contact.opted_outOpt-out: palavra-chave, recusa 131050 da Meta, painel, API ou fluxo
broadcast.completedUma campanha terminou de enviar
broadcast.pausedUma campanha parou — por alguém ou sozinha
flow.run.completedUm contato chegou ao fim de um fluxo
flow.answer.capturedUm passo de Pergunta ou de Aguardar resposta guardou uma resposta
conversation.assignedUma conversa foi entregue a alguém da equipe
conversation.closedUma conversa foi fechada, por uma pessoa ou por um fluxo

Um endpoint só gera esses eventos se alguém os assina: sem nenhum endpoint ativo que os receba, nada é gravado. Depois de criar ou editar um endpoint, conte até trinta — é o tempo que a lista de assinaturas leva para ser relida.

Contatos​

Todo evento que fala de um contato traz o mesmo objeto contact: o suficiente para achá-lo de novo, sem mais dados pessoais.

contact.opted_out
{
"field": "contact.opted_out",
"value": {
"contact": {
"id": "cm1c0ntact0001",
"phone": "+5541987111527",
"wa_id": "5541987111527",
"bsuid": null,
"first_name": "Ana",
"last_name": "Souza",
"email": null
},
"via": "keyword",
"reason": "PARAR",
"source": "joinotify",
"occurred_at": "2026-09-19T14:02:11.000Z"
}
}

via diz de onde veio a mudança: manual (painel), api, flow, inbound (a primeira mensagem do cliente criou o contato), keyword (a pessoa mandou uma palavra de saída) ou meta_131050 (a Meta recusou um envio porque a pessoa deixou de aceitar marketing). reason só aparece no opt-out — numa palavra-chave, é o texto que a pessoa mandou.

Eventos de contato são da conta

contact.* sai sem wabaId e sem phoneNumberId, como a cobrança: um contato é da conta, não de um número. Um endpoint filtrado por número não os recebe.

Campanhas​

broadcast.paused
{
"field": "broadcast.paused",
"value": {
"broadcast": {
"id": "cm1bro4dc4st01",
"name": "Black Friday",
"template": "promo_black_friday",
"language": "pt_BR",
"phone_number_id": "106540352242922",
"flow_id": null,
"recipients": 4800,
"sent": 1210,
"delivered": 1175,
"read": 640,
"failed": 12,
"clicked": 88,
"replied": 31,
"opted_out": 4
},
"reason": "throughput",
"detail": "130429",
"resume_after": "2026-11-27T13:55:00.000Z",
"source": "joinotify",
"occurred_at": "2026-11-27T13:40:00.000Z"
}
}

Os números são os do momento em que o evento saiu. Os status de uma campanha concluída continuam chegando por dias, então broadcast.completed traz a foto do fechamento, não o resultado final. Ele leva finished_at no lugar de reason.

reasonPor que a campanha parou
manualAlguém pausou
qualityA qualidade do número ficou vermelha, ou a Meta aplicou o limite de spam (131048)
templateO template foi pausado, desativado, apagado ou ficou com qualidade vermelha
accountO número foi desconectado ou a conta está bloqueada, restrita ou sem pagamento na Meta
quotaO plano atingiu o teto mensal de mensagens
billingA conta perdeu o direito de uso
errorsFalhas demais em sequência, ou erros de parâmetro logo no começo — costuma ser o mapeamento de variáveis
throughputA Meta segurou a vazão por minutos. Retoma sozinha em 15 min, 1 h e 4 h; resume_after diz quando
send_windowFora da janela de envio configurada. Retoma sozinha na próxima abertura

detail complementa o motivo — o código da Meta, o status do template (PAUSED, DELETED, RED…) — e pode vir nulo.

Fluxos​

flow.run.completed
{
"field": "flow.run.completed",
"value": {
"flow": { "id": "cm1fl0w000001", "name": "Captura de lead", "version": 4 },
"run": {
"id": "cm1run0000001",
"trigger_type": "api",
"end_reason": "end",
"started_at": "2026-09-19T14:00:02.000Z",
"finished_at": "2026-09-19T14:03:40.000Z"
},
"contact": {
"id": "cm1c0ntact0001",
"phone": "+5541987111527",
"wa_id": "5541987111527",
"bsuid": null,
"first_name": "Ana",
"last_name": "Souza",
"email": null
},
"variables": { "empresa": "Padaria Central", "interesse": "orçamento" },
"source": "joinotify",
"occurred_at": "2026-09-19T14:03:40.000Z"
}
}

variables traz todas as variáveis do fluxo no fim da execução — inclusive as respostas que o contato deu. Execuções de teste nunca geram eventos.

flow.answer.captured
{
"field": "flow.answer.captured",
"value": {
"flow": { "id": "cm1fl0w000001", "name": "Captura de lead", "version": 4 },
"run_id": "cm1run0000001",
"node_id": "question_2",
"variable": "empresa",
"field": null,
"value": "Padaria Central",
"contact": {
"id": "cm1c0ntact0001",
"phone": "+5541987111527",
"wa_id": "5541987111527",
"bsuid": null,
"first_name": "Ana",
"last_name": "Souza",
"email": null
},
"source": "joinotify",
"occurred_at": "2026-09-19T14:01:15.000Z"
}
}

variable é o nome da variável do fluxo que recebeu a resposta; field, o campo do contato, quando o passo grava direto num campo. Um dos dois pode vir nulo.

Conversas​

conversation.assigned
{
"field": "conversation.assigned",
"value": {
"conversation": { "id": "cm1c0nv000001", "phone_number_id": "106540352242922" },
"contact": {
"id": "cm1c0ntact0001",
"phone": "+5541987111527",
"wa_id": "5541987111527",
"bsuid": null,
"first_name": "Ana",
"last_name": "Souza",
"email": null
},
"assigned_user_id": "cm1us3r000001",
"assigned_by": "flow:cm1fl0w000001",
"source": "joinotify",
"occurred_at": "2026-09-19T14:03:41.000Z"
}
}

assigned_by e closed_by dizem quem agiu: o id de uma pessoa da equipe, flow:<id> quando foi um fluxo, ou null quando foi uma API key. conversation.closed traz só closed_by.

Campanhas, fluxos e conversas saem com o número (phoneNumberId) e a conta empresarial dele, então um endpoint filtrado por número ou por WABA recebe os eventos desse número.

Consultando o histórico​

Se um evento se perdeu antes de você ter um endpoint no ar:

curl 'https://api.joinotify.com/events?field=messages&limit=50' \
-H 'Authorization: Bearer sk_live_xxx'

O campo payload é o change.value original, sem alteração.