Catálogo de eventos

Cada evento que a BotoZap emite, a categoria que o Endpoint precisa assinar para recebê-lo, a chave de idempotência e o payload completo.

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.

CategoriaO que entrega
messagesMensagens recebidas e mudanças de Consentimento (crm.contact.consent_changed, apesar do prefixo crm.).
statusesStatus das mensagens enviadas pela BotoZap (enviada, entregue, lida, falhou) e o término de uma execução de Régua (automation.journey_run.finished).
crmTodo Evento crm.* exceto Consentimento: mudança de etapa do contato, oportunidades, demandas e agendamentos (criados e atualizados).
accountSaú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.

EventoCategoriaNo streamX-Idempotency-Key
whatsapp.message.receivedmessagessim<wamid>
whatsapp.message.sentstatusessim<wamid>:sent
whatsapp.message.deliveredstatusessim<wamid>:delivered
whatsapp.message.readstatusessim<wamid>:read
whatsapp.message.failedstatusessim<wamid>:failed
whatsapp.account.updatedaccountsimwhatsapp.account.updated:<sha256 do payload normalizado>
whatsapp.phone_number.quality_updatedaccountsimwhatsapp.phone_number.quality_updated:<sha256 do payload normalizado>
crm.contact.stage_changedcrmsimcrm:stage:<contact_id>:<transação>:<etapa anterior|none>><nova etapa|none>
crm.contact.consent_changedmessagessimconsent:<id do registro de consentimento>
crm.opportunity.createdcrmsimcrm:opportunity:<id>:<version>
crm.opportunity.updatedcrmsimcrm:opportunity:<id>:<version>
crm.demand.createdcrmsimcrm:demand:<id>:<version>
crm.demand.updatedcrmsimcrm:demand:<id>:<version>
crm.appointment_createdcrmsimcrm:appointment:<id>:created
crm.appointment_updatedcrmsimappointment:<id>:<revision>
automation.journey_run.finishedstatusessimjourney_run:<run_id>:<status>
webhook.testnenhumanãotest:<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 (o wamid no 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": {}
  }
}

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 wamid nã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 gera crm.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 vale 0 (desde o início). Qualquer outro valor responde 422 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 seguir paging.cursor percorre 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 }
}
CampoSignificado
idUUID do evento no stream; estável, use para deduplicar leituras.
cursorPosição do evento, como string de inteiro.
typeO nome fino do evento (o mesmo do webhook).
environmentlive ou sandbox: o ambiente do evento, o mesmo da chave que leu o stream e o mesmo campo do corpo do webhook.
message_idO wamid quando o evento é de uma mensagem do WhatsApp; null nos demais.
external_idO identificador da mensagem no canal; null em eventos sem mensagem.
message_resource_idO id da mensagem na BotoZap (o mesmo de GET /api/v1/messages), quando existe.
occurred_atQuando o fato aconteceu (horário da Meta quando disponível).
created_atQuando a BotoZap gravou o evento.
dataO payload do evento; veja as diferenças por tipo acima.
paging.cursorCursor do último item da página (ou o próprio after numa página vazia). Persista-o.
paging.next / paging.has_morenext 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.