Webhooks
O Botzetta avisa o seu app quando algo acontece numa empresa que o instalou, com um POST assinado
para a URL de webhook cadastrada no Portal do parceiro.
O aviso leva só o sinal: o tipo do evento e os ids envolvidos, nunca nome, telefone ou conteúdo.
Para os detalhes, chame a API com o token daquela empresa (ex.: GET /oportunidades/{id}). Assim o
dado sempre passa pelos escopos em vigor.
Webhooks são entregues a apps de parceiro. Chaves de integração não recebem webhooks.
Eventos
Os eventos que o app recebe são os declarados na configuração do app no portal, desde que a empresa tenha consentido o escopo de leitura correspondente.
| Evento | Escopo exigido | recurso.tipo | contexto |
|---|---|---|---|
app.instalado, app.escopos-alterados | — | instalacao | escopos |
app.desinstalado | — | instalacao | — |
pessoa.criada, pessoa.atualizada | pessoas:ler | pessoa | — |
oportunidade.criada, oportunidade.ganha, oportunidade.perdida | crm:ler | oportunidade | pessoaId, pipelineId, estagioId |
oportunidade.movida | crm:ler | oportunidade | pessoaId, pipelineId, estagioId, estagioAnteriorId |
ticket.aberto (criado ou reaberto), ticket.fechado | tickets:ler | ticket | contatoId, filaId |
ticket.transferido | tickets:ler | ticket | contatoId, filaId, filaAnteriorId |
mensagem.recebida, mensagem.enviada | mensagens:ler | mensagem | ticketId, contatoId |
- Os eventos
app.*chegam sempre. - Vale o escopo em vigor: uma versão nova do app que pede
pessoas:lersó recebepessoa.*depois que a empresa consentir. - Notas internas não geram
mensagem.*. - O botão "Enviar evento de teste" do portal manda o evento
webhook.teste, com ocompanyIdda sua sandbox erecurso: { "tipo": "teste", "id": 0 }, assinado como os demais. Responda 2xx.
Formato da entrega
POST /webhooks/botzetta HTTP/1.1
Host: app.exemplo.com
Content-Type: application/json
X-Botzetta-Evento: oportunidade.ganha
X-Botzetta-Entrega: ent_6f1c2a4e-…
X-Botzetta-Assinatura: t=1793103240,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
"id": "evt_0b7d9c1e-…",
"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"
}
| Campo | Significado |
|---|---|
id | Id do evento. Igual em todas as tentativas e reenvios: use para deduplicar |
evento | Tipo do evento (tabela acima) |
versao | Versão do formato do evento ("1") |
companyId | Empresa onde o fato aconteceu. Use o token desta empresa para buscar os detalhes |
recurso | Tipo e id do registro afetado |
contexto | Ids relacionados (varia por evento) |
ocorridoEm | Quando o fato aconteceu (UTC) |
X-Botzetta-Entrega identifica a entrega e se repete nas novas tentativas e no reenvio manual.
Verificar a assinatura
O cabeçalho X-Botzetta-Assinatura tem o formato t=<unix>,v1=<hex>, em que v1 é o
HMAC-SHA256, em hexadecimal, de <t>.<corpo cru> com o segredo do webhook (whsec_…, exibido uma
única vez no portal).
- Leia o corpo cru, antes de qualquer parse de JSON.
- Separe
te todos osv1do cabeçalho. - Recuse se
testiver a mais de 5 minutos do relógio do seu servidor. - Calcule
HMAC-SHA256(segredo, t + "." + corpo)e compare em tempo constante com cadav1. Basta um conferir.
Durante a troca do segredo (rotação no portal), o segredo anterior continua assinando por 7 dias,
e o cabeçalho leva dois valores: t=…,v1=<novo>,v1=<anterior>.
Exemplo em Node.js, sem bibliotecas além do Express:
import crypto from "node:crypto";
import express from "express";
const app = express();
const SEGREDO = process.env.BOTZETTA_WEBHOOK_SEGREDO; // whsec_…
function assinaturaValida(corpo, cabecalho, segredo) {
const partes = String(cabecalho ?? "").split(",").map(p => p.trim().split("="));
const t = partes.find(([k]) => k === "t")?.[1];
const assinaturas = partes.filter(([k]) => k === "v1").map(([, v]) => v);
if (!t || !assinaturas.length) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // mais de 5 min
const esperada = crypto.createHmac("sha256", segredo).update(`${t}.`).update(corpo).digest();
return assinaturas.some(v => {
const recebida = Buffer.from(v, "hex");
return recebida.length === esperada.length && crypto.timingSafeEqual(recebida, esperada);
});
}
app.post("/webhooks/botzetta", express.raw({ type: "application/json" }), (req, res) => {
if (!assinaturaValida(req.body, req.get("x-botzetta-assinatura"), SEGREDO)) return res.sendStatus(401);
const evento = JSON.parse(req.body.toString("utf8"));
filaDeEventos.adicionar(evento); // processe depois; responda rápido
res.sendStatus(204);
});
Responder e processar
- Responda 2xx em até 10 segundos. Processe o evento depois, numa fila sua.
- A entrega é "pelo menos uma vez": o mesmo evento pode chegar mais de uma vez. Deduplique pelo
id. - A ordem não é garantida. Use
ocorridoEme, na dúvida, busque o estado atual pela API. - Redirect (3xx), timeout e qualquer status fora de 2xx contam como falha.
Retentativas e desativação
| Tentativa | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| Espera desde a anterior | na hora | 1 min | 5 min | 30 min | 1 h | 2 h | 4 h | 8 h | 8 h |
- Depois da 9ª falha, a entrega fica como
falhou. - Se o endpoint falhar continuamente por 3 dias, o webhook é desativado. Os eventos novos ficam retidos por até 7 dias. Ao reativar no portal, as entregas retidas e as que falharam com evento de até 7 dias são reenviadas.
- Na hora de cada envio, a instalação, o plano e o escopo são conferidos de novo: se deixaram de valer, a entrega é descartada.
- No portal você vê o histórico de entregas de 30 dias e pode reenviar uma entrega.
Requisitos da URL
- Só
https://, com certificado válido. - Não pode resolver para IP privado ou de rede interna.
- Redirects não são seguidos.
Obrigações
- Guarde o segredo do webhook só no servidor.
- Ao receber
app.desinstalado, apague os dados daquela empresa em até 30 dias (LGPD).