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
| Evento | Quando dispara |
|---|---|
messages | Mensagem recebida, ou mudança de status de uma que você enviou |
smb_message_echoes | Mensagem enviada pelo aplicativo do celular (coexistência) |
history | Blocos do histórico sincronizado |
smb_app_state_sync | Contatos 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).
{
"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?" }
}
]
}
}
{
"field": "messages",
"value": {
"statuses": [
{
"id": "wamid.HBgN...",
"status": "delivered",
"timestamp": "1785312042",
"recipient_id": "5541999999999",
"conversation": { "id": "...", "origin": { "type": "service" } },
"pricing": { "billable": true, "category": "service" }
}
]
}
}
status | Significado |
|---|---|
sent | Saiu da infraestrutura da Meta |
delivered | Chegou ao aparelho |
read | Foi aberta, se o cliente não desativou a confirmação |
failed | Nã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
| Evento | Quando dispara |
|---|---|
message_template_status_update | Aprovado, rejeitado, pausado ou desabilitado |
message_template_quality_update | Qualidade mudou (GREEN / YELLOW / RED) |
message_template_components_update | Conteúdo do template foi alterado |
template_category_update | A 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
| Evento | Quando dispara |
|---|---|
account_alerts | Alerta da Meta sobre a conta |
account_update | Mudança no estado da conta empresarial |
account_review_update | Resultado da revisão da conta |
account_settings_update | Configurações alteradas |
account_update inclui banimentos e restrições. Vale monitorar: é o aviso de que a
operação vai parar.
Número
| Evento | Quando dispara |
|---|---|
phone_number_name_update | Resultado da solicitação de nome de exibição |
phone_number_quality_update | Qualidade 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
| Evento | Quando dispara |
|---|---|
business_capability_update | Limites e capacidades mudaram |
business_status_update | Status de verificação do negócio mudou |
Outros
| Evento | Quando dispara |
|---|---|
security | Eventos de segurança da conta |
flows | Interações com WhatsApp Flows |
calls | Eventos de chamada |
user_preferences | Cliente 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".
| Evento | Quando dispara |
|---|---|
billing.past_due | O pagamento falhou e a tolerância começou. Ainda envia |
billing.blocked | Acesso cortado: as rotas de envio passam a responder 402 |
billing.restored | Voltou 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.
{
"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.
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".
| Evento | Quando dispara |
|---|---|
wallet.credited | Entrou saldo: comissão, ajuste do operador ou devolução de saque falho |
wallet.debited | Saiu saldo: aplicado numa fatura, ou reservado por um pedido de saque |
affiliate.approved | A inscrição no programa foi aprovada e o código passou a valer |
affiliate.rejected | A inscrição foi recusada ou suspensa — reason diz por quê |
payout.paid | O saque foi pago, com method (pix/paypal) e payment_ref |
{
"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".
| Evento | Quando dispara |
|---|---|
contact.created | Um contato novo: primeira mensagem, API, fluxo ou painel. Importações não anunciam linha a linha |
contact.opted_in | Consentimento de marketing registrado — pelo painel, pela API ou por um fluxo |
contact.opted_out | Opt-out: palavra-chave, recusa 131050 da Meta, painel, API ou fluxo |
broadcast.completed | Uma campanha terminou de enviar |
broadcast.paused | Uma campanha parou — por alguém ou sozinha |
flow.run.completed | Um contato chegou ao fim de um fluxo |
flow.answer.captured | Um passo de Pergunta ou de Aguardar resposta guardou uma resposta |
conversation.assigned | Uma conversa foi entregue a alguém da equipe |
conversation.closed | Uma 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.
{
"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.
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
{
"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.
reason | Por que a campanha parou |
|---|---|
manual | Alguém pausou |
quality | A qualidade do número ficou vermelha, ou a Meta aplicou o limite de spam (131048) |
template | O template foi pausado, desativado, apagado ou ficou com qualidade vermelha |
account | O número foi desconectado ou a conta está bloqueada, restrita ou sem pagamento na Meta |
quota | O plano atingiu o teto mensal de mensagens |
billing | A conta perdeu o direito de uso |
errors | Falhas demais em sequência, ou erros de parâmetro logo no começo — costuma ser o mapeamento de variáveis |
throughput | A Meta segurou a vazão por minutos. Retoma sozinha em 15 min, 1 h e 4 h; resume_after diz quando |
send_window | Fora 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
{
"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.
{
"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
{
"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.