Webhooks
Webhooks de saída
Os eventos, o corpo de cada envio, as retentativas e a assinatura.
O tracker avisa outro sistema quando algo muda numa issue: um POST em JSON, assinado, para o endereço que a empresa cadastrar. Admin e dono da empresa criam os webhooks no app, em Empresa → Integrações → Webhooks (até 20 por empresa), escolhendo os eventos e se o webhook vale para todos os projetos ou só para um. A mesma tela mostra o segredo, as últimas entregas com a resposta do receptor, o botão de reenviar e o envio de teste (ping). Criar e mudar webhooks não existe na API nem no MCP.
Eventos:
issue.created: Issue criada. Pela tela, por e-mail ou pelo MCP.issue.updated: Issue editada. Campos, registro técnico, release, etiquetas, épico, sprint, estimativa e prazo. Vem com o que mudou.issue.status_changed: Status mudou. Inclusive pelo reteste (passou ou falhou).issue.assigned: Responsável mudou. Responsável ou QA responsável.comment.created: Comentário novo. Inclusive respostas e respostas por e-mail.automation.triggered: uma regra de automação do projeto com a ação "Enviar webhook" disparou. Vai só para o webhook que a regra escolheu, assine ele o evento ou não, e trazautomation.rule(id,name).ping: o envio de teste da tela, commessage.
Corpo (JSON), igual em todo evento: id (do evento; o mesmo nas retentativas, use para não processar duas vezes), event, created_at, organization (id, slug, name), actor (id, name: quem fez a mudança) e issue, o retrato da issue na hora: id, code, title, url, type, status (a chave do fluxo), status_name, status_category (todo, in_progress, done), severity, priority, project (id, name, key), assignee, qa_owner e reporter (id, name, ou null), labels, release_id, fixed_version, description, custom_fields, created_at, updated_at e resolved_at. issue.updated, issue.status_changed e issue.assigned trazem changes, campo a campo, {"status": {"from": "open", "to": "doing"}} (as chaves do histórico da issue). comment.created traz comment (id, body em markdown, parent_id quando é resposta, author, mentions e url).
Cabeçalhos: Content-Type: application/json, User-Agent: tracker-by-aquarela-webhooks/1.0, X-Tracker-Event (o evento), X-Tracker-Delivery (o id da entrega) e X-Tracker-Signature.
Entrega: só endereço https público (rede interna é recusada), sem seguir redirecionamento, com 10 s para responder. Qualquer 2xx conta como entregue; outra resposta, ou nenhuma, tenta de novo até 6 vezes, depois de 1 min, 5 min, 30 min, 2 h, 6 h. Responda rápido e processe depois: a ordem de chegada não é garantida.
Assinatura: X-Tracker-Signature: t=<unix>,v1=<hex>, em que v1 é o HMAC-SHA256, em hexadecimal, do texto <t>.<corpo cru> com o segredo inteiro do webhook (whsec_...) como chave. Recalcule com o corpo exatamente como chegou (antes de interpretar o JSON), compare em tempo constante e recuse t com mais de 5 minutos de diferença. Durante a troca do segredo o cabeçalho pode trazer mais de um v1: aceite se um deles conferir. Em Node:
import { createHmac, timingSafeEqual } from "node:crypto";
// body: o corpo cru da requisição (string), antes do JSON.parse
function isFromTracker(body, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // mais de 5 min
const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}