Webhooks
Webhooks de salida
Los eventos, el cuerpo de cada envío, los reintentos y la firma.
tracker avisa a otro sistema cuando algo cambia en una issue: un POST en JSON, firmado, a la dirección que registre la empresa. Los admins y dueños de la empresa crean los webhooks en la app, en Empresa → Integraciones → Webhooks (hasta 20 por empresa), eligiendo los eventos y si el webhook vale para todos los proyectos o solo para uno. La misma pantalla muestra el secreto, las últimas entregas con la respuesta del receptor, el botón de reenviar y el envío de prueba (ping). Crear y cambiar webhooks no existe en la API ni en el MCP.
Eventos:
issue.created: Tarea creada. Desde la pantalla, por correo o por el MCP.issue.updated: Tarea editada. Campos, registro técnico, release, etiquetas, épica, sprint, estimación y plazo. Viene con lo que cambió.issue.status_changed: Estado cambió. Incluso por la reprueba (pasó o falló).issue.assigned: Responsable cambió. Responsable o QA responsable.comment.created: Comentario nuevo. Incluso respuestas y respuestas por correo.automation.triggered: se disparó una regla de automatización del proyecto con la acción "Enviar webhook". Va solo al webhook que eligió la regla, esté suscrito al evento o no, y traeautomation.rule(id,name).ping: el envío de prueba de la pantalla, conmessage.
Cuerpo (JSON), igual en todo evento: id (del evento; el mismo en los reintentos, úsalo para no procesar dos veces), event, created_at, organization (id, slug, name), actor (id, name: quien hizo el cambio) e issue, el retrato de la issue en ese momento: id, code, title, url, type, status (la clave del flujo), status_name, status_category (todo, in_progress, done), severity, priority, project (id, name, key), assignee, qa_owner y reporter (id, name, o null), labels, release_id, fixed_version, description, custom_fields, created_at, updated_at y resolved_at. issue.updated, issue.status_changed e issue.assigned traen changes, campo por campo, {"status": {"from": "open", "to": "doing"}} (las claves del historial de la issue). comment.created trae comment (id, body en markdown, parent_id cuando es respuesta, author, mentions y url).
Encabezados: Content-Type: application/json, User-Agent: tracker-by-aquarela-webhooks/1.0, X-Tracker-Event (el evento), X-Tracker-Delivery (el id de la entrega) y X-Tracker-Signature.
Entrega: solo direcciones https públicas (la red interna se rechaza), sin seguir redirecciones, con 10 s para responder. Cualquier 2xx cuenta como entregado; otra respuesta, o ninguna, se reintenta hasta 6 veces, después de 1 min, 5 min, 30 min, 2 h, 6 h. Responde rápido y procesa después: el orden de llegada no está garantizado.
Firma: X-Tracker-Signature: t=<unix>,v1=<hex>, donde v1 es el HMAC-SHA256, en hexadecimal, del texto <t>.<cuerpo crudo> con el secreto completo del webhook (whsec_...) como clave. Recalcúlalo con el cuerpo exactamente como llegó (antes de interpretar el JSON), compara en tiempo constante y rechaza t con más de 5 minutos de diferencia. Durante el cambio del secreto el encabezado puede traer más de un v1: acéptalo si alguno coincide. En 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);
}