Pular para o conteúdo principal

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.

EventoEscopo exigidorecurso.tipocontexto
app.instalado, app.escopos-alterados—instalacaoescopos
app.desinstalado—instalacao—
pessoa.criada, pessoa.atualizadapessoas:lerpessoa—
oportunidade.criada, oportunidade.ganha, oportunidade.perdidacrm:leroportunidadepessoaId, pipelineId, estagioId
oportunidade.movidacrm:leroportunidadepessoaId, pipelineId, estagioId, estagioAnteriorId
ticket.aberto (criado ou reaberto), ticket.fechadotickets:lerticketcontatoId, filaId
ticket.transferidotickets:lerticketcontatoId, filaId, filaAnteriorId
mensagem.recebida, mensagem.enviadamensagens:lermensagemticketId, contatoId
  • Os eventos app.* chegam sempre.
  • Vale o escopo em vigor: uma versão nova do app que pede pessoas:ler só recebe pessoa.* 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 o companyId da sua sandbox e recurso: { "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"
}
CampoSignificado
idId do evento. Igual em todas as tentativas e reenvios: use para deduplicar
eventoTipo do evento (tabela acima)
versaoVersão do formato do evento ("1")
companyIdEmpresa onde o fato aconteceu. Use o token desta empresa para buscar os detalhes
recursoTipo e id do registro afetado
contextoIds relacionados (varia por evento)
ocorridoEmQuando 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).

  1. Leia o corpo cru, antes de qualquer parse de JSON.
  2. Separe t e todos os v1 do cabeçalho.
  3. Recuse se t estiver a mais de 5 minutos do relógio do seu servidor.
  4. Calcule HMAC-SHA256(segredo, t + "." + corpo) e compare em tempo constante com cada v1. 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 ocorridoEm e, 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​

Tentativa123456789
Espera desde a anteriorna hora1 min5 min30 min1 h2 h4 h8 h8 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).