Pular para o conteúdo principal

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​

  1. Cadastre a URL do webhook (https:) em Configuração › Cadastro. Na primeira vez, o portal mostra o segredo whsec_… uma única vez.
  2. No rascunho, marque os eventos que o app quer receber e publique a versão.
  3. Teste com Enviar evento de teste (aba Webhooks e logs).

Os eventos app.* chegam sempre, marcados ou não.

Eventos (v1)​

EventoEscopo exigidorecurso.tipocontexto
app.instalado—instalacaoescopos
app.desinstalado—instalacao—
app.escopos-alterados—instalacaoescopos
pessoa.criada, pessoa.atualizadapessoas:lerpessoa—
oportunidade.criada, oportunidade.movida, oportunidade.ganha, oportunidade.perdidacrm:leroportunidadepessoaId, pipelineId, estagioId (+ estagioAnteriorId em movida)
ticket.aberto (criado ou reaberto), ticket.transferido, ticket.fechadotickets:lerticketcontatoId, filaId (+ filaAnteriorId em transferido)
mensagem.recebida, mensagem.enviadamensagens:lermensagemticketId, 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.

  1. Leia o corpo cru (os bytes que chegaram). Não faça JSON.parse e JSON.stringify antes: o resultado muda espaços e ordem, e a assinatura não confere.
  2. Separe t e os v1 do cabeçalho.
  3. Recuse se t estiver a mais de 5 minutos do seu relógio (para trás ou para frente).
  4. Calcule o HMAC e compare com cada v1 em tempo constante. Basta um conferir.
  5. 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 exemplo GET /oportunidades/{id} (getOportunidadesId) para oportunidade.*.
  • Em app.desinstalado, apague os tokens da empresa e programe a exclusão dos dados dela.

Retentativas e desativação​

SituaçãoO que acontece
Resposta 2xxEntregue
Outro status, redirecionamento ou timeoutNova tentativa: +1 min, 5 min, 30 min, 1 h, 2 h, 4 h, 8 h e 8 h (9 no total)
Tentativas esgotadasA entrega fica falhou
Endpoint falhando há 3 diasO 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 valerA 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 rotaFaz
GET /plataforma/v1/webhookEstado: { url, eventos, status, desativadoEm, motivo, falhandoDesde }
GET /plataforma/v1/webhook/entregasLog (?status=&evento=&companyId=&limite=), paginado por antesDe
POST /plataforma/v1/webhook/entregas/{id}/reenviarReenvia uma entrega (mesmo id de evento)
POST /plataforma/v1/webhook/reativarReativa 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.