Webhooks
O Botzetta avisa o app de fatos das empresas que o instalaram com um POST assinado na URL do
webhook do cadastro. O corpo leva só o sinal (qual evento, qual registro): os detalhes você
busca na API com o token da empresa. Assim nenhum dado pessoal trafega no webhook.
A referência de webhooks resume o formato; esta página mostra como receber.
Configurar
- Cadastre a URL do webhook (
https:) em Configuração › Cadastro. Na primeira vez, o portal mostra o segredowhsec_…uma única vez. - No rascunho, marque os eventos que o app quer receber e publique a versão.
- Teste com Enviar evento de teste (aba Webhooks e logs).
Os eventos app.* chegam sempre, marcados ou não.
Eventos (v1)
| Evento | Escopo exigido | recurso.tipo | contexto |
|---|---|---|---|
app.instalado | — | instalacao | escopos |
app.desinstalado | — | instalacao | — |
app.escopos-alterados | — | instalacao | escopos |
pessoa.criada, pessoa.atualizada | pessoas:ler | pessoa | — |
oportunidade.criada, oportunidade.movida, oportunidade.ganha, oportunidade.perdida | crm:ler | oportunidade | pessoaId, pipelineId, estagioId (+ estagioAnteriorId em movida) |
ticket.aberto (criado ou reaberto), ticket.transferido, ticket.fechado | tickets:ler | ticket | contatoId, filaId (+ filaAnteriorId em transferido) |
mensagem.recebida, mensagem.enviada | mensagens:ler | mensagem | ticketId, contatoId |
O escopo vale o efetivo da empresa: uma versão nova que pede pessoas:ler só recebe pessoa.*
de uma empresa depois que ela consentir. Notas internas não geram mensagem.*.
A requisição
POST https://meuapp.exemplo.com/botzetta/webhook
Content-Type: application/json
X-Botzetta-Evento: oportunidade.ganha
X-Botzetta-Entrega: ent_6c1f…
X-Botzetta-Assinatura: t=1793451240,v1=5f2c…e9a1
{
"id": "evt_0d4b…",
"evento": "oportunidade.ganha",
"versao": "1",
"companyId": 88,
"recurso": { "tipo": "oportunidade", "id": 5531 },
"contexto": { "pessoaId": 12, "pipelineId": 3, "estagioId": 9 },
"ocorridoEm": "2026-10-27T12:14:00.000Z"
}
id(evt_…) identifica o evento: é igual em todas as tentativas e no reenvio.X-Botzetta-Entrega(ent_…) identifica a entrega no log do portal.ocorridoEmé quando o fato aconteceu. Use para ordenar: a ordem de chegada não é garantida.
Verificar a assinatura
X-Botzetta-Assinatura tem o formato t=<unix>,v1=<hex>, em que v1 é o
HMAC-SHA256 do texto t + "." + corpo, com o segredo do webhook como chave, em hexadecimal.
- Leia o corpo cru (os bytes que chegaram). Não faça
JSON.parseeJSON.stringifyantes: o resultado muda espaços e ordem, e a assinatura não confere. - Separe
te osv1do cabeçalho. - Recuse se
testiver a mais de 5 minutos do seu relógio (para trás ou para frente). - Calcule o HMAC e compare com cada
v1em tempo constante. Basta um conferir. - Só então faça o parse do JSON.
import { createHmac, timingSafeEqual } from "node:crypto";
function verificarAssinatura(corpoCru, cabecalho, segredo) {
if (!cabecalho) return null;
const partes = cabecalho.split(",").map(p => p.split("="));
const t = partes.find(([k]) => k === "t")?.[1];
const assinaturas = partes.filter(([k]) => k === "v1").map(([, v]) => v);
if (!/^\d+$/.test(t ?? "") || Math.abs(Date.now() / 1000 - Number(t)) > 300) return null;
const esperado = createHmac("sha256", segredo).update(`${t}.`).update(corpoCru).digest();
const confere = assinaturas.some(v => /^[0-9a-f]{64}$/.test(v) && timingSafeEqual(Buffer.from(v, "hex"), esperado));
return confere ? JSON.parse(corpoCru.toString("utf8")) : null;
}
Assinatura inválida: responda 401 e descarte. Corpo que não é JSON: responda 400 (um 5xx faria o Botzetta tentar de novo à toa). O exemplo ponta a ponta tem a versão
completa, com testes.
Rotação do segredo
Ao rotacionar o segredo (aba Credenciais), o anterior continua assinando por 7 dias. Nesse período
o cabeçalho leva dois v1: t=…,v1=<novo>,v1=<anterior>. Se o seu código aceita qualquer v1 que
confira, basta trocar o segredo no servidor dentro desse prazo.
Responder e processar
- Responda 2xx em até 10 segundos. Coloque o evento numa fila e processe depois.
- Deduplique pelo
id: a entrega é "pelo menos uma vez" e o mesmo evento pode chegar mais de uma vez. - Busque os detalhes na API com o token da empresa do
companyId, por exemploGET /oportunidades/{id}(getOportunidadesId) paraoportunidade.*. - Em
app.desinstalado, apague os tokens da empresa e programe a exclusão dos dados dela.
Retentativas e desativação
| Situação | O que acontece |
|---|---|
| Resposta 2xx | Entregue |
| Outro status, redirecionamento ou timeout | Nova tentativa: +1 min, 5 min, 30 min, 1 h, 2 h, 4 h, 8 h e 8 h (9 no total) |
| Tentativas esgotadas | A entrega fica falhou |
| Endpoint falhando há 3 dias | O webhook é desativado; eventos novos ficam retidos por até 7 dias |
| Reativar (portal) | As retidas e as que falharam nos últimos 7 dias são reentregues |
| Na hora do envio a instalação, o plano ou o escopo deixou de valer | A entrega é descartada |
O log das entregas fica 30 dias em Webhooks e logs, com reenvio individual.
O destino tem de ser https:, sem IP privado. O Botzetta não segue redirecionamentos.
Gerenciar pela API
O próprio app pode consultar e operar o webhook, autenticado com as credenciais do app em
Authorization: Basic base64(client_id:client_secret) (limite de 120 por minuto):
| Método e rota | Faz |
|---|---|
GET /plataforma/v1/webhook | Estado: { url, eventos, status, desativadoEm, motivo, falhandoDesde } |
GET /plataforma/v1/webhook/entregas | Log (?status=&evento=&companyId=&limite=), paginado por antesDe |
POST /plataforma/v1/webhook/entregas/{id}/reenviar | Reenvia uma entrega (mesmo id de evento) |
POST /plataforma/v1/webhook/reativar | Reativa um webhook desativado |
Evento de teste
O botão Enviar evento de teste manda evento: "webhook.teste", com o companyId da sua sandbox
e recurso: { "tipo": "teste", "id": 0 }, assinado como os demais. Ele não aparece no log de
entregas e não tem retentativa: confira a assinatura e responda 2xx.