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.
{
"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" }
}
}
}| Campo | O que é |
|---|---|
id | Id 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. |
type | O evento (tabela abaixo). |
created_at | Quando o fato aconteceu. A ordem de chegada não é garantida (novas tentativas chegam depois): use este campo para ordenar. |
bot | O chatbot, com o nome; null nos eventos da conta (account.*). |
client | O 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). |
conversation | A conversa e o canal, ou null nos eventos sem conversa. |
contact | Quem é: ids do canal (phone, whatsapp_user_id, instagram_id) e o nome, nunca o conteúdo da conversa. null nos eventos sem contato. |
data | O que aconteceu. |
Eventos
Mensagens
| Evento | Quando | data |
|---|---|---|
message.received | O 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.sent | Saiu 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.status | A Meta confirmou entrega ou leitura. | message {id, status}: delivered ou read. Desmarcado por padrão: são até 2 eventos por mensagem enviada. |
message.failed | A mensagem não chegou ao contato. | message e error {code} (o código da Meta). |
message.deleted | Uma 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
| Evento | Quando | data |
|---|---|---|
lead.created | A IA registrou um contato novo para a equipe. | lead {id, name, phone, email, notes}. |
lead.updated | O 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.requested | A 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.returned | A conversa voltou para a IA. | reason (agent_resumed, timeout, api) e agent. |
conversation.assigned | A 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.transferred | Algué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.answered | O 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
| Evento | Quando | data |
|---|---|---|
ticket.created | Algué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.updated | O 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
| Evento | Quando | data |
|---|---|---|
routine.completed | O 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.abandoned | O 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
| Evento | Quando | data |
|---|---|---|
contact.opted_in | Aceitou receber novidades (ou lembretes). | consent {channel, type, source, at}; type marketing, utility ou all. |
contact.opted_out | Pediu para não receber (SAIR, botão ou o próprio WhatsApp). | O mesmo consent. |
contact.linked | O 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.unlinked | O 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.deleted | Pedido 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
| Evento | Quando | data |
|---|---|---|
campaign.finished | Uma 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.connected | WhatsApp ou Instagram conectado. | channel. |
channel.disconnected | O canal caiu ou foi desconectado. | channel e reason {code, message}. |
channel.issue | Problema no canal sem desconectar (hoje: a conta do WhatsApp sem forma de pagamento). | channel e issue {code, message}. |
template.status_changed | A Meta aprovou, recusou ou pausou um modelo do número. | template {name, language, category, status, reason}. |
bot.paused | O chatbot foi pausado. | bot, paused_by e reason. |
bot.resumed | O chatbot voltou. | bot e resumed_by. |
compliance.changed | Mudou 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.
| Evento | Quando | data |
|---|---|---|
bot.quota_warning | O chatbot chegou a 80% ou 95% da cota do mês. | level (80 ou 95) e o uso. |
bot.overage_started | O primeiro atendimento a mais do mês: a cota acabou com o excedente ligado. | O uso. |
bot.overage_cap_warning | O excedente chegou a 80% ou 95% do teto (só quando há teto). | level e o uso. |
bot.quota_reached | O 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_changed | O 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_changed | O 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_closed | Nos 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_low | O crédito da conta ficou abaixo de 20% da última compra. | balance_brl, last_purchase_brl, low_threshold_brl e auto_recharge. |
account.credit_depleted | O crédito não paga o próximo atendimento a mais. | Os mesmos campos. |
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.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.
// 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.statuse 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.