BoaVoz
Documentação · Integrações

Webhooks

O BoaVoz avisa o seu sistema do que acontece nas conversas e nos canais: mensagem recebida ou enviada, lead, pedido de atendente, aceite de novidades, canal caiu, modelo aprovado e mais.

Configurar

No painel, menu Integrações → Webhooks (dono ou administrador da agência, com a verificação em duas etapas). Cada webhook tem:

  • a URL (HTTPS, num endereço público);
  • os eventos que ele quer receber;
  • o escopo: todos os chatbots da agência, os de um cliente ou chatbots escolhidos;
  • cabeçalhos personalizados (opcional, até 10, guardados cifrados);
  • o segredo (whsec_…), mostrado uma vez ao criar.

Enviar teste manda um webhook.test assinado. Em Logs aparecem as entregas, com o corpo e a sua resposta; Reenviar falhas desde põe de volta na fila o que falhou a partir de uma data.

O envelope

Todo evento chega no mesmo formato. O que muda de um evento para outro é o data.

json
{
  "id": "evt_k3x2f7…",
  "type": "message.received",
  "created_at": "2026-10-15T19:02:11.482Z",
  "bot": { "id": "bot_8c1e…", "name": "Bar do Zé" },
  "client": { "id": "cli_51a0…", "external_id": null },
  "conversation": { "id": "conv_14a2…", "channel": "whatsapp" },
  "contact": {
    "id": "ctc_77d0…",
    "whatsapp_user_id": null,
    "phone": "5511999990000",
    "instagram_id": null,
    "display": { "name": "Ruan" }
  },
  "data": {
    "message": {
      "id": "msg_48213",
      "text": "Pedir este: Pizza Calabresa",
      "media": null,
      "interactive": { "type": "button_reply", "id": "pedir_101", "title": "Pedir este" }
    }
  }
}
CampoO que é
idId do evento (evt_…). Sai do próprio fato: a nova tentativa e o reenvio trazem o mesmo id. Use para não processar duas vezes.
typeO evento (tabela abaixo).
created_atQuando o fato aconteceu. A ordem de chegada não é garantida (novas tentativas chegam depois): use este campo para ordenar.
botO chatbot, com o nome; null nos eventos da conta (account.*).
clientO cliente (o negócio) do chatbot, ou null. O external_id é o id dele no seu sistema, quando ele foi criado pela API (senão, null).
conversationA conversa e o canal, ou null nos eventos sem conversa.
contactQuem é: ids do canal (phone, whatsapp_user_id, instagram_id) e o nome, nunca o conteúdo da conversa. null nos eventos sem contato.
dataO que aconteceu.

Eventos

Mensagens

EventoQuandodata
message.receivedO contato mandou uma mensagem (ou tocou num botão).message {id, text, media, interactive}. interactive traz o id do botão que você definiu e o título.
message.sentSaiu uma mensagem para o contato: da IA, da equipe, da API, de campanha ou do app do celular.message, source (ai, human, api, campaign, phone_app) e agent (quem enviou).
message.statusA Meta confirmou entrega ou leitura.message {id, status}: delivered ou read. Desmarcado por padrão: são até 2 eventos por mensagem enviada.
message.failedA mensagem não chegou ao contato.message e error {code} (o código da Meta).
message.deletedUma mensagem foi desfeita (o contato apagou no Instagram, ou no WhatsApp quando o número também está no app do celular).message {id}. As entregas pendentes dela saem da fila antes.

Arquivo na mensagem: media traz {id, type, mime, size, filename, transcript, reading, url, url_expires_at}. O url vale 1 hora (é gerado de novo a cada tentativa); depois, baixe por GET /v1/media/{id}. Áudio vem com a transcrição; foto que a IA leu vem com reading, o que ela entendeu da foto (veja em Arquivos e fotos).

Atendimento

EventoQuandodata
lead.createdA IA registrou um contato novo para a equipe.lead {id, name, phone, email, notes}.
lead.updatedO mesmo contato voltou com um pedido novo (ou mudou o contato). Um lead por contato em cada chatbot.lead e request {created_at, channel, conversation_id, text} (null quando só o contato mudou).
handoff.requestedA conversa entrou na fila da equipe.reason: requested (pediu atendente), lead (deixou um pedido) ou ai_unavailable; priority (urgent em risco à vida); outside_hours.
handoff.returnedA conversa voltou para a IA.reason (agent_resumed, timeout, api) e agent.
conversation.assignedA conversa chegou a uma pessoa da equipe: ela assumiu, o rodízio do setor entregou ou veio numa transferência.agent {id, display_name, type}, by (agent, rotation ou transfer), previous_agent (quem estava com a conversa, ou null) e sector {id, name} (ou null).
conversation.transferredAlguém da equipe transferiu a conversa para outra pessoa ou para um setor. A nota interna não vai no evento.from (quem transferiu) e to {type, agent, sector}: type agent ou sector; no setor com rodízio, agent é quem recebeu.
survey.answeredO contato respondeu a pesquisa de satisfação (WhatsApp e Instagram). Só a primeira resposta de cada pesquisa.survey {id, sent_at, trigger} (agent_finished: no fim do atendimento humano; ai_idle: depois de 30 minutos de conversa parada com a IA), score (3, 2 ou 1), rating (great, ok ou bad) e answered_at.

Chamados

EventoQuandodata
ticket.createdAlguém abriu um chamado no chat do site.ticket {protocol, status, subject}, requester {name, email, phone} (quem abriu) e attachment {id, mime, size, filename} (a foto ou o PDF do chamado, quando o chatbot aceita anexo; baixe por GET /v1/media/{id}), ou null.
ticket.updatedO chamado mudou: a equipe trocou o status, respondeu, ou a pessoa escreveu de novo pelo acompanhamento.ticket, change (status, resposta_equipe ou mensagem_cliente), previous_status e agent (quem da equipe, quando foi a equipe). Status: aberto, em_andamento, aguardando_cliente e resolvido. A nota interna nunca vai.

Para quem já tem um helpdesk (Zendesk, Freshdesk…): o ticket.created leva o necessário para criar o chamado lá, com o protocolo do BoaVoz como referência; o ticket.updated mantém os dois iguais.

Roteiros

EventoQuandodata
routine.completedO roteiro chegou ao fim.routine {id, name}, run {id, started_by, started_at, ended_at, step, steps_total} e variables (os dados que o roteiro juntou, {type, value}, no formato das ações).
routine.abandonedO roteiro acabou antes do fim.O mesmo (as variables que deu tempo de juntar), mais reason: handoff, cancelled, timeout, idle, replaced, routine_changed, step_unavailable ou context_changed (veja Fim do roteiro).

Contato

EventoQuandodata
contact.opted_inAceitou receber novidades (ou lembretes).consent {channel, type, source, at}; type marketing, utility ou all.
contact.opted_outPediu para não receber (SAIR, botão ou o próprio WhatsApp).O mesmo consent.
contact.linkedO contato foi ligado a uma conta do seu sistema (pareamento, ou número lembrado pelo roteiro com a licença da pessoa).link {id, channel, source, external_id, phone_masked}; source pairing ou routine.
contact.unlinkedO vínculo foi desfeito.link e reason (api, chat, identity_changed, inactivity, other_person: a pessoa disse que não é quem estava lembrado no número, ou remember_off: a loja desligou o lembrar o número).
contact.deletedPedido do titular (LGPD) concluído: os dados do contato foram apagados.reason e request_id; o contact traz só id e external_id. Apague do seu lado também.

Campanhas, canais e conformidade

EventoQuandodata
campaign.finishedUma campanha ou lote de lembretes terminou.campaign com os totais: enviadas, entregues, lidas, falhas, puladas (sem aceite, sem 18+, na supressão) e saídas.
channel.connectedWhatsApp ou Instagram conectado.channel.
channel.disconnectedO canal caiu ou foi desconectado.channel e reason {code, message}.
channel.issueProblema no canal sem desconectar (hoje: a conta do WhatsApp sem forma de pagamento).channel e issue {code, message}.
template.status_changedA Meta aprovou, recusou ou pausou um modelo do número.template {name, language, category, status, reason}.
bot.pausedO chatbot foi pausado.bot, paused_by e reason.
bot.resumedO chatbot voltou.bot e resumed_by.
compliance.changedMudou a situação do cliente nas regras de uso.compliance {status, previous_status, restrictions, …}.

Cota, excedente e crédito

Todos trazem o uso do mês no data: month, used (dentro da cota), quota e overage {enabled, used, cap_brl, max, value_brl}. Cada aviso sai uma vez por mês e chatbot.

EventoQuandodata
bot.quota_warningO chatbot chegou a 80% ou 95% da cota do mês.level (80 ou 95) e o uso.
bot.overage_startedO primeiro atendimento a mais do mês: a cota acabou com o excedente ligado.O uso.
bot.overage_cap_warningO excedente chegou a 80% ou 95% do teto (só quando há teto).level e o uso.
bot.quota_reachedO chatbot parou de abrir atendimentos com a IA (modo só humano) até o mês virar ou alguém agir.reason: cota (cheia, sem excedente), teto, sem_credito, excedente_suspenso (pico no chat do site) ou primeira_semana (nos 7 dias do início da assinatura, o chatbot usou 25% da cota; libera no 8º dia); e o uso.
bot.size_changedO tamanho mudou, foi agendado para o dia 1º ou o agendado saiu.status (applied, scheduled, cancelled), size, volume_packs, previous e effective_at.
bot.overage_changedO excedente foi ligado, desligado, mudou de teto, ficou suspenso (pico ou ataque) ou voltou.enabled, cap_brl e suspended (pico, ataque ou null).
bot.month_closedNos primeiros dias do mês, o fechamento do mês anterior de cada chatbot com tamanho ou atendimento.month, size e quota que valeram, atendimentos, overage_atendimentos, overage_value_brl e next (o tamanho de agora).
account.credit_lowO crédito da conta ficou abaixo de 20% da última compra.balance_brl, last_purchase_brl, low_threshold_brl e auto_recharge.
account.credit_depletedO crédito não paga o próximo atendimento a mais.Os mesmos campos.
Eventos da conta
account.credit_low e account.credit_depleted são da conta, não de um chatbot: vêm com bot null e só para os webhooks com escopo de toda a conta.
Aceite do negócio
No WhatsApp e no Instagram, os eventos com conteúdo de conversa ou de contato (mensagens, lead, atendimento e consentimento) só saem depois que o negócio aceita os termos de uso daquele canal, na conexão. Os de campanha, canal, modelo, pausa e conformidade saem sempre.

Receber e conferir

O BoaVoz faz um POST com o envelope, assinado no padrão Standard Webhooks com o segredo do webhook: cabeçalhos webhook-id (o id do evento), webhook-timestamp e webhook-signature. Confira com a biblioteca oficial e responda 2xx rápido.

javascript · node
// npm i express standardwebhooks
import express from "express";
import { Webhook } from "standardwebhooks";

const app = express();
const wh = new Webhook(process.env.BOAVOZ_WEBHOOK_SECRET); // whsec_... do webhook

app.post("/boavoz/eventos", express.text({ type: "*/*" }), async (req, res) => {
  let evento;
  try {
    evento = wh.verify(req.body, req.headers); // assinatura e horário (até 5 minutos)
  } catch {
    return res.status(401).end();
  }
  // a nova tentativa traz o mesmo id: processe uma vez só
  if (await jaProcessado(evento.id)) return res.status(200).end();
  await fila.publicar(evento); // responda rápido; o trabalho pesado vai para a sua fila
  res.status(200).end();
});

Entrega e novas tentativas

  • Entregue = 2xx em até 10 segundos. Qualquer outra coisa conta como falha.
  • Novas tentativas, com o mesmo id: 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h e 24 h depois (9 tentativas no total). Cada tentativa vai com horário e assinatura novos.
  • 410 Gone desativa o webhook na hora. 3 dias seguidos só de falhas também desativam. O dono da conta recebe um e-mail com 1 hora de falhas, outro no segundo dia e outro quando o webhook é desativado, com o link para reativar. O painel mostra o motivo; reative e use “Reenviar falhas desde” para recuperar o que ficou para trás.
  • Até 10 entregas ao mesmo tempo por webhook: a partir da 11ª, o evento espera na fila e sai no minuto seguinte, na ordem. Os eventos de alto volume (message.status e os envios de campanha) sempre saem pela fila.
  • Um pedido do titular (LGPD) apaga as entregas do contato, inclusive as pendentes, antes do contact.deleted.

Para testar sem servidor, use o Svix Play: ele mostra os eventos com os cabeçalhos e o corpo. Veja também Ações, que usam a mesma assinatura.