Esta página é a referência dos nomes finos de evento. Para cadastrar, verificar e auditar um Endpoint, veja Webhooks; para retry, ordem e duplicatas, veja Confiabilidade e entrega.
Categorias de assinatura
Um Endpoint assina categorias, não nomes finos: POST /api/v1/webhooks aceita em events apenas messages, statuses, crm e account. Valores fora dessa lista são descartados, e uma lista sem nenhum valor válido responde 422 invalid_events. Assinar uma categoria entrega todos os eventos dela, inclusive os que forem adicionados depois.
| Categoria | O que entrega |
|---|---|
messages | Mensagens recebidas e mudanças de Consentimento (crm.contact.consent_changed, apesar do prefixo crm.). |
statuses | Status das mensagens enviadas pela BotoZap (enviada, entregue, lida, falhou) e o término de uma execução de Régua (automation.journey_run.finished). |
crm | Todo Evento crm.* exceto Consentimento: mudança de etapa do contato, oportunidades, demandas e agendamentos (criados e atualizados). |
account | Saúde da WABA: banimentos, restrições, violações e mudança de limite de mensagens do número. |
Duas exceções ao prefixo do nome: crm.contact.consent_changed pertence a messages (um Endpoint assinado só em crm não o recebe) e automation.journey_run.finished pertence a statuses. O evento webhook.test não pertence a categoria nenhuma: vai só para o Endpoint que você testou.
Todos os eventos
| Evento | Categoria | No stream | X-Idempotency-Key |
|---|---|---|---|
whatsapp.message.received | messages | sim | <wamid> |
whatsapp.message.sent | statuses | sim | <wamid>:sent |
whatsapp.message.delivered | statuses | sim | <wamid>:delivered |
whatsapp.message.read | statuses | sim | <wamid>:read |
whatsapp.message.failed | statuses | sim | <wamid>:failed |
whatsapp.account.updated | account | sim | whatsapp.account.updated:<sha256 do payload normalizado> |
whatsapp.phone_number.quality_updated | account | sim | whatsapp.phone_number.quality_updated:<sha256 do payload normalizado> |
crm.contact.stage_changed | crm | sim | crm:stage:<contact_id>:<transação>:<etapa anterior|none>><nova etapa|none> |
crm.contact.consent_changed | messages | sim | consent:<id do registro de consentimento> |
crm.opportunity.created | crm | sim | crm:opportunity:<id>:<version> |
crm.opportunity.updated | crm | sim | crm:opportunity:<id>:<version> |
crm.demand.created | crm | sim | crm:demand:<id>:<version> |
crm.demand.updated | crm | sim | crm:demand:<id>:<version> |
crm.appointment_created | crm | sim | crm:appointment:<id>:created |
crm.appointment_updated | crm | sim | appointment:<id>:<revision> |
automation.journey_run.finished | statuses | sim | journey_run:<run_id>:<status> |
webhook.test | nenhuma | não | test:<endpoint_id> |
Envelope comum
Todo corpo é um objeto JSON com event (o mesmo valor do header X-Webhook-Event), customer_id (o Cliente dono da origem do evento; null quando a cadeia não pôde ser resolvida e no webhook.test) e environment (live ou sandbox, o ambiente da Conta de canal de origem; null só no webhook.test, que não nasce de canal nenhum). Os eventos ligados a um canal de mensagem também trazem o carimbo de canal:
channel: o canal da origem, ex.whatsapp.channel_account: a Conta de canal de origem, como{ id, channel, display }.external_id: o identificador da mensagem no canal (owamidno WhatsApp). Ausente nos eventos de CRM e de automação, que não se referem a uma mensagem.
Os eventos de saúde da WABA (account) e o webhook.test não têm carimbo de canal. A ordem das chaves não é garantida; leia os campos pelo nome.
Categoria messages
whatsapp.message.received
Um contato envia uma mensagem para um número conectado (inclusive respostas de botão e de lista). X-Idempotency-Key: <wamid>. Também entra no stream GET /api/v1/events.
phone_number_id: O phone_number_id da Meta que recebeu a mensagem.message: O objeto de mensagem cru da Cloud API (id, from, timestamp, type, o conteúdo do tipo e context quando é resposta ou botão), mais external_id. Quando o contato chega por anúncio Click-to-WhatsApp, traz o referral da Meta (source_id, source_url, source_type, headline, ctwa_clid…; ctwa_clid não vem em anúncios de Status).contact: wa_id (telefone ou, sem telefone, o BSUID), user_id, username, phone, profile_name e metadata (os campos personalizados do contato).
No stream, data.message é normalizado pela BotoZap (id, external_id, resource_id, conversation_id, contact_id, direction, type, status, content, context, timestamp) em vez do objeto cru da Cloud API, e data.contact inclui o id do contato.
{
"event": "whatsapp.message.received",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"external_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"phone_number_id": "1279498075235551",
"message": {
"from": "5511988887777",
"id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"timestamp": "1790341200",
"type": "text",
"text": {
"body": "Oi, quero saber do meu pedido"
},
"external_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE"
},
"contact": {
"wa_id": "5511988887777",
"user_id": null,
"username": null,
"phone": "5511988887777",
"profile_name": "Maria",
"metadata": {}
}
}crm.contact.consent_changed
Um Consentimento do contato é registrado ou revogado (painel, importação, API, palavra-chave ou botão de conversa). X-Idempotency-Key: consent:<id do registro de consentimento>. Também entra no stream GET /api/v1/events.
contact: { id } do contato.channel: Canal do Consentimento.purpose: Finalidade: utility ou marketing.state: granted ou revoked.origin: manual, import, api, keyword ou conversation_button.evidence: Texto de evidência (até 500 caracteres; vazio quando não informado).occurred_at: Momento do registro.
{
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"event": "crm.contact.consent_changed",
"contact": {
"id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b"
},
"purpose": "marketing",
"state": "revoked",
"origin": "keyword",
"evidence": "SAIR",
"occurred_at": "2026-09-25T13:00:00.000Z",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live"
}Categoria statuses
O status do WhatsApp vira um nome fino por valor: whatsapp.message.sent, whatsapp.message.delivered, whatsapp.message.read e whatsapp.message.failed. Todos têm os mesmos campos, e status repete o sufixo do nome. Só são emitidos para mensagens registradas na BotoZap (enviadas pela API, pelo painel, por transmissão ou por Régua); status de mensagens desconhecidas e os valores deleted e warning não geram evento. A Meta pode entregar os status fora de ordem: use a máquina de estados do seu lado, não a ordem de chegada.
whatsapp.message.sent
A Meta confirma que aceitou e enviou uma mensagem da BotoZap (API, painel, transmissão ou Régua). X-Idempotency-Key: <wamid>:sent. Também entra no stream GET /api/v1/events.
phone_number_id: O phone_number_id da Meta do número que enviou.status: O status cru da Meta: sent, delivered, read ou failed (igual ao sufixo do nome do evento).message_id: O wamid da mensagem enviada (igual a external_id).recipient_id: O destinatário informado pela Meta; null quando ausente.pricing: O objeto pricing da Meta (billable, pricing_model, category…) quando vier; senão null.conversation: Só nos envios dentro da janela grátis do Free Entry Point (anúncio Click-to-WhatsApp): conversation.id e, no sent, expiration_timestamp com o fim da janela. Ausente nos demais.errors: A lista errors da Meta (code, title, message, error_data) — relevante em failed; senão null.
No stream, campos nulos são omitidos e message_resource_id aponta para a mensagem quando ela existe em GET /api/v1/messages.
{
"event": "whatsapp.message.sent",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"external_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"phone_number_id": "1279498075235551",
"status": "sent",
"message_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"recipient_id": "5511988887777",
"pricing": {
"billable": true,
"pricing_model": "PMP",
"category": "marketing",
"type": "regular"
},
"errors": null
}whatsapp.message.delivered
A Meta confirma a entrega no aparelho do contato. X-Idempotency-Key: <wamid>:delivered. Também entra no stream GET /api/v1/events.
phone_number_id: O phone_number_id da Meta do número que enviou.status: O status cru da Meta: sent, delivered, read ou failed (igual ao sufixo do nome do evento).message_id: O wamid da mensagem enviada (igual a external_id).recipient_id: O destinatário informado pela Meta; null quando ausente.pricing: O objeto pricing da Meta (billable, pricing_model, category…) quando vier; senão null.conversation: Só nos envios dentro da janela grátis do Free Entry Point (anúncio Click-to-WhatsApp): conversation.id e, no sent, expiration_timestamp com o fim da janela. Ausente nos demais.errors: A lista errors da Meta (code, title, message, error_data) — relevante em failed; senão null.
No stream, campos nulos são omitidos e message_resource_id aponta para a mensagem quando ela existe em GET /api/v1/messages.
{
"event": "whatsapp.message.delivered",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"external_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"phone_number_id": "1279498075235551",
"status": "delivered",
"message_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"recipient_id": "5511988887777",
"pricing": {
"billable": true,
"pricing_model": "PMP",
"category": "marketing",
"type": "regular"
},
"errors": null
}whatsapp.message.read
A Meta informa que o contato leu a mensagem. X-Idempotency-Key: <wamid>:read. Também entra no stream GET /api/v1/events.
phone_number_id: O phone_number_id da Meta do número que enviou.status: O status cru da Meta: sent, delivered, read ou failed (igual ao sufixo do nome do evento).message_id: O wamid da mensagem enviada (igual a external_id).recipient_id: O destinatário informado pela Meta; null quando ausente.pricing: O objeto pricing da Meta (billable, pricing_model, category…) quando vier; senão null.conversation: Só nos envios dentro da janela grátis do Free Entry Point (anúncio Click-to-WhatsApp): conversation.id e, no sent, expiration_timestamp com o fim da janela. Ausente nos demais.errors: A lista errors da Meta (code, title, message, error_data) — relevante em failed; senão null.
No stream, campos nulos são omitidos e message_resource_id aponta para a mensagem quando ela existe em GET /api/v1/messages.
{
"event": "whatsapp.message.read",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"external_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"phone_number_id": "1279498075235551",
"status": "read",
"message_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"recipient_id": "5511988887777",
"pricing": {
"billable": true,
"pricing_model": "PMP",
"category": "marketing",
"type": "regular"
},
"errors": null
}whatsapp.message.failed
A Meta informa que a mensagem falhou depois de aceita. X-Idempotency-Key: <wamid>:failed. Também entra no stream GET /api/v1/events.
phone_number_id: O phone_number_id da Meta do número que enviou.status: O status cru da Meta: sent, delivered, read ou failed (igual ao sufixo do nome do evento).message_id: O wamid da mensagem enviada (igual a external_id).recipient_id: O destinatário informado pela Meta; null quando ausente.pricing: O objeto pricing da Meta (billable, pricing_model, category…) quando vier; senão null.conversation: Só nos envios dentro da janela grátis do Free Entry Point (anúncio Click-to-WhatsApp): conversation.id e, no sent, expiration_timestamp com o fim da janela. Ausente nos demais.errors: A lista errors da Meta (code, title, message, error_data) — relevante em failed; senão null.
No stream, campos nulos são omitidos e message_resource_id aponta para a mensagem quando ela existe em GET /api/v1/messages.
{
"event": "whatsapp.message.failed",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"external_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"phone_number_id": "1279498075235551",
"status": "failed",
"message_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"recipient_id": "5511988887777",
"pricing": null,
"errors": [
{
"code": 131026,
"title": "Message undeliverable",
"message": "Message undeliverable",
"error_data": {
"details": "Message Undeliverable."
}
}
]
}automation.journey_run.finished
Uma execução de Régua termina: completed, stopped (parada manual, cancelamento de agendamento, etc.) ou failed. Só para contatos de um número do WhatsApp. X-Idempotency-Key: journey_run:<run_id>:<status>. Também entra no stream GET /api/v1/events.
status: completed, stopped ou failed.stop_reason: Motivo da parada (ex.: manual, appointment_cancelled); null quando não se aplica.journey: { id } da Régua.run: { id, occurrence_key, opportunity_id, demand_id } da execução.contact: { id } do contato.
{
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"event": "automation.journey_run.finished",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"status": "completed",
"stop_reason": null,
"journey": {
"id": "3b4c5d6e-7f8a-4b9c-8d0e-1f2a3b4c5d6e"
},
"run": {
"id": "4c5d6e7f-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"occurrence_key": "stage:6c1b9d2f-3e4a-4b5c-9d7e-8f9a0b1c2d3e",
"opportunity_id": null,
"demand_id": null
},
"contact": {
"id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b"
}
}Categoria crm
crm.contact.stage_changed
A etapa do contato muda: kanban, PATCH /api/v1/contacts/{id}, promoção automática por resposta de transmissão ou etapa excluída. X-Idempotency-Key: crm:stage:<contact_id>:<transação>:<etapa anterior|none>><nova etapa|none>. Também entra no stream GET /api/v1/events.
phone_number_id: O phone_number_id da Meta do número do contato; ausente fora do WhatsApp.contact: id, wa_id, user_id, username, phone e profile_name (campos nulos omitidos).previous_stage / new_stage: { id, key, label } da etapa anterior e da nova; ausentes ao entrar no funil ou sair dele.changed_by: manual (kanban/API), auto (transmissão/resposta) ou system (etapa limpa ou excluída).occurred_at: Momento da mudança.
{
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"event": "crm.contact.stage_changed",
"phone_number_id": "1279498075235551",
"contact": {
"id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"wa_id": "5511988887777",
"phone": "5511988887777",
"profile_name": "Maria"
},
"previous_stage": {
"id": "5b0a8c1e-2d3f-4a5b-8c6d-7e8f9a0b1c2d",
"key": "negociando",
"label": "Negociando"
},
"new_stage": {
"id": "6c1b9d2f-3e4a-4b5c-9d7e-8f9a0b1c2d3e",
"key": "fechou",
"label": "Fechou"
},
"changed_by": "manual",
"occurred_at": "2026-09-25T13:00:00.000Z",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live"
}crm.opportunity.created
Uma oportunidade é criada (painel, API, MCP ou agente). X-Idempotency-Key: crm:opportunity:<id>:<version>. Também entra no stream GET /api/v1/events.
entity_type: Sempre opportunity.entity_id: O id da entidade.before_data: null na criação.after_data: A linha inteira depois da gravação (inclui version, status e contact_id).actor_user_id: Usuário do painel que fez a alteração; null via API/automação.
{
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"event": "crm.opportunity.created",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"entity_type": "opportunity",
"entity_id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b",
"before_data": null,
"after_data": {
"id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b",
"contact_id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"title": "Plano anual",
"status": "open",
"value_cents": 120000,
"currency": "BRL",
"version": 1
},
"actor_user_id": null
}crm.opportunity.updated
Qualquer alteração gravada na oportunidade (cada gravação incrementa version e gera um evento). X-Idempotency-Key: crm:opportunity:<id>:<version>. Também entra no stream GET /api/v1/events.
entity_type: Sempre opportunity.entity_id: O id da entidade.before_data: A linha inteira antes da alteração.after_data: A linha inteira depois da gravação (inclui version, status e contact_id).actor_user_id: Usuário do painel que fez a alteração; null via API/automação.
{
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"event": "crm.opportunity.updated",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"entity_type": "opportunity",
"entity_id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b",
"before_data": {
"version": 1,
"status": "open"
},
"after_data": {
"id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b",
"contact_id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"title": "Plano anual",
"status": "open",
"value_cents": 120000,
"currency": "BRL",
"version": 2
},
"actor_user_id": null
}crm.demand.created
Uma demanda é criada (painel, API, MCP ou agente). X-Idempotency-Key: crm:demand:<id>:<version>. Também entra no stream GET /api/v1/events.
entity_type: Sempre demand.entity_id: O id da entidade.before_data: null na criação.after_data: A linha inteira depois da gravação (inclui version, status e contact_id).actor_user_id: Usuário do painel que fez a alteração; null via API/automação.
{
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"event": "crm.demand.created",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"entity_type": "demand",
"entity_id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b",
"before_data": null,
"after_data": {
"id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b",
"contact_id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"title": "Troca de produto",
"status": "open",
"version": 1
},
"actor_user_id": null
}crm.demand.updated
Qualquer alteração gravada na demanda (cada gravação incrementa version e gera um evento). X-Idempotency-Key: crm:demand:<id>:<version>. Também entra no stream GET /api/v1/events.
entity_type: Sempre demand.entity_id: O id da entidade.before_data: A linha inteira antes da alteração.after_data: A linha inteira depois da gravação (inclui version, status e contact_id).actor_user_id: Usuário do painel que fez a alteração; null via API/automação.
{
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"event": "crm.demand.updated",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"entity_type": "demand",
"entity_id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b",
"before_data": {
"version": 1,
"status": "open"
},
"after_data": {
"id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b",
"contact_id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"title": "Troca de produto",
"status": "in_progress",
"version": 2
},
"actor_user_id": null
}crm.appointment_created
Um agendamento é criado. X-Idempotency-Key: crm:appointment:<id>:created. Também entra no stream GET /api/v1/events.
phone_number_id: O phone_number_id da Meta do número do contato; ausente fora do WhatsApp.appointment: id, contact_id, scheduled_at, note e created_at (campos nulos omitidos).contact: id, wa_id, phone e profile_name do contato.
{
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"event": "crm.appointment_created",
"phone_number_id": "1279498075235551",
"appointment": {
"id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
"contact_id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"scheduled_at": "2026-09-30T14:00:00.000Z",
"created_at": "2026-09-25T13:00:00.000Z"
},
"contact": {
"id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"wa_id": "5511988887777",
"phone": "5511988887777",
"profile_name": "Maria"
},
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live"
}crm.appointment_updated
Qualquer alteração gravada num agendamento: remarcação, confirmação, cancelamento, falta ou edição. X-Idempotency-Key: appointment:<id>:<revision>. Também entra no stream GET /api/v1/events.
appointment: A linha inteira do agendamento depois da alteração (inclui status, scheduled_at e revision).before_data: A linha antes da alteração.after_data: A linha depois da alteração (igual a appointment).
{
"channel": "whatsapp",
"channel_account": {
"id": "8d1f3c2a-6b7e-4f1a-9c3d-2e5b7a9c1d40",
"channel": "whatsapp",
"display": "+55 11 3333-4444"
},
"event": "crm.appointment_updated",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"appointment": {
"id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
"contact_id": "a3c1e2f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"status": "cancelled",
"scheduled_at": "2026-09-30T14:00:00.000Z",
"revision": 2
},
"before_data": {
"status": "confirmed",
"revision": 1
},
"after_data": {
"status": "cancelled",
"revision": 2
}
}Categoria account
whatsapp.account.updated
A Meta envia o field account_update para a WABA (restrição, violação de política, banimento, desbloqueio). X-Idempotency-Key: whatsapp.account.updated:<sha256 do payload normalizado>. Também entra no stream GET /api/v1/events.
waba_id: A WABA afetada.account_event: O evento da Meta, ex.: ACCOUNT_RESTRICTION, ACCOUNT_VIOLATION, DISABLED_UPDATE.ban_state: O estado de banimento conhecido; null quando não informado.restrictions: Lista de { restriction_type, expiration }; lista vazia = sem restrições; null quando não informado.violation_type: Só em ACCOUNT_VIOLATION, ex.: SPAM.occurred_at: Horário do evento na Meta; null quando ilegível.
{
"event": "whatsapp.account.updated",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"waba_id": "4207187232868022",
"account_event": "ACCOUNT_RESTRICTION",
"ban_state": null,
"restrictions": [
{
"restriction_type": "RESTRICTED_BIZ_INITIATED_MESSAGING",
"expiration": "2026-08-31T00:00:00.000Z"
}
],
"occurred_at": "2026-08-27T12:00:00.000Z"
}whatsapp.phone_number.quality_updated
A Meta envia o field phone_number_quality_update (apesar do nome, informa o tier de limite de mensagens do número). X-Idempotency-Key: whatsapp.phone_number.quality_updated:<sha256 do payload normalizado>. Também entra no stream GET /api/v1/events.
waba_id: A WABA do número.phone_number_id: O phone_number_id da Meta do número afetado.display_phone_number: O número como a Meta exibe.messaging_limit_tier: O tier de limite, ex.: TIER_2K.occurred_at: Horário do evento na Meta; null quando ilegível.
{
"event": "whatsapp.phone_number.quality_updated",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"waba_id": "4207187232868022",
"phone_number_id": "1279498075235551",
"display_phone_number": "+1 555-078-3881",
"messaging_limit_tier": "TIER_2K",
"occurred_at": "2026-08-27T12:00:00.000Z"
}Evento de teste
webhook.test
POST /api/v1/webhooks/{id}/test ou o botão Testar e ativar do painel. X-Idempotency-Key: test:<endpoint_id>. Não entra no stream GET /api/v1/events.
message: Texto fixo: Evento de teste do BotoZap.sent_at: Momento do envio.
{
"event": "webhook.test",
"customer_id": null,
"environment": null,
"message": "Evento de teste do BotoZap.",
"sent_at": "2026-09-25T13:00:00.000Z"
}A chave é a mesma em todos os testes do Endpoint; não deduplique testes por ela. O teste fica registrado em GET /api/v1/webhook_deliveries com event_type=webhook.test.
O que não gera evento
- Aprovação, rejeição ou pausa de template. A BotoZap atualiza o status do template, mas não emite evento. Consulte
GET /api/v1/templates. - Ecos de coexistência. Uma mensagem que o atendente envia pelo app WhatsApp Business entra no histórico da conversa, mas o eco dela não gera evento. Edições e remoções de mensagem, feitas pelo app ou pelo contato, também não.
- Status de mensagem desconhecida. Status cujo
wamidnão corresponde a uma mensagem registrada na BotoZap são descartados. - Edições de CRM fora dos eventos listados. Criar ou editar o contato (nome, campos personalizados, notas), configurar as etapas do funil e excluir o contato (junto com as oportunidades, demandas e agendamentos dele) não geram evento. Criar ou alterar oportunidade e demanda gera
crm.opportunity.*/crm.demand.*; mudar a etapa do contato geracrm.contact.stage_changed. - Início e passos de uma Régua. Só o término da execução gera
automation.journey_run.finished.
Sandbox e produção no mesmo Endpoint
Endpoints de webhook pertencem à Conta, não ao ambiente. Eventos gerados pelo número Sandbox e pelos números de produção chegam ao mesmo Endpoint; separe-os pelo campo environment do corpo: sandbox para o que nasceu no Sandbox, live para produção. Eventos da Conta sem canal, como os de saúde da WABA (account), são sempre live: o Sandbox não os simula. O stream GET /api/v1/events já é separado por ambiente e traz o mesmo environment em cada item (veja abaixo).
Stream durável: GET /api/v1/events
Além do POST no seu Endpoint, a BotoZap grava os eventos num log append-only por Conta e ambiente. O stream não depende de Endpoint cadastrado nem de assinatura de categoria: ele recebe tudo o que a tabela acima marca como "sim", e é a forma de recuperar um evento que o seu Endpoint perdeu (queda, pausa por saúde ou retenção vencida).
- Escopo
events:read. A chave Sandbox lê o stream do Sandbox; a chave live lê o de produção. O cursor é independente em cada ambiente. after: inteiro não negativo, exclusivo; omitido vale0(desde o início). Qualquer outro valor responde422 invalid_cursor.limit: padrão 20, máximo 100. Valor acima do máximo é reduzido a 100; valor inválido volta ao padrão.- A ordem é ascendente por
cursor, contíguo por Conta e ambiente: repetir a mesma chamada devolve a mesma página, e seguirpaging.cursorpercorre o intervalo sem lacunas.
curl "https://botozap.com.br/api/v1/events?after=41&limit=50" \
-H "X-API-Key: $BOTOZAP_KEY"{
"data": [
{
"id": "0e6f1c2a-9b3d-4e5f-8a7b-6c5d4e3f2a1b",
"cursor": "42",
"type": "whatsapp.message.delivered",
"environment": "live",
"message_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"external_id": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYIDNBMjE",
"message_resource_id": "e1f2a3b4-c5d6-4e7f-8a9b-1c2d3e4f5a6b",
"occurred_at": "2026-09-25T13:00:03.000Z",
"created_at": "2026-09-25T13:00:04.120Z",
"data": { "event": "whatsapp.message.delivered", "status": "delivered", "…": "…" }
}
],
"paging": { "cursor": "42", "next": "42", "has_more": true }
}| Campo | Significado |
|---|---|
id | UUID do evento no stream; estável, use para deduplicar leituras. |
cursor | Posição do evento, como string de inteiro. |
type | O nome fino do evento (o mesmo do webhook). |
environment | live ou sandbox: o ambiente do evento, o mesmo da chave que leu o stream e o mesmo campo do corpo do webhook. |
message_id | O wamid quando o evento é de uma mensagem do WhatsApp; null nos demais. |
external_id | O identificador da mensagem no canal; null em eventos sem mensagem. |
message_resource_id | O id da mensagem na BotoZap (o mesmo de GET /api/v1/messages), quando existe. |
occurred_at | Quando o fato aconteceu (horário da Meta quando disponível). |
created_at | Quando a BotoZap gravou o evento. |
data | O payload do evento; veja as diferenças por tipo acima. |
paging.cursor | Cursor do último item da página (ou o próprio after numa página vazia). Persista-o. |
paging.next / paging.has_more | next repete o cursor quando há mais itens; null quando has_more é false. |
Retenção: o stream não expira por tempo. Os eventos saem junto com a exclusão da Conta ou com a exclusão dos dados de uma Conta de canal. Excluir um contato (DELETE /api/v1/contacts/{id}) apaga os eventos dele: os das mensagens dele e os que trazem o contato no payload. Esses eventos deixam de aparecer no stream, e os cursores dos demais não mudam.
Stream e webhooks juntos
O mesmo fato pode chegar pelos dois caminhos, e as chaves são diferentes: no webhook, deduplique por X-Idempotency-Key; no stream, por id. Um padrão seguro é usar o webhook para reagir em tempo real e rodar, periodicamente ou depois de uma queda, a leitura do stream a partir do último paging.cursor salvo, processando só o que ainda não foi visto. O stream não depende da saúde do Endpoint: um Endpoint em paused ou unverified não impede a gravação dos eventos. O webhook.test nunca entra no stream.
