Webhooks
Outgoing webhooks
The events, the body of each delivery, retries and the signature.
tracker notifies another system when something changes in an issue: a signed JSON POST to the address the company registers. Company admins and owners create webhooks in the app, under Company → Integrations → Webhooks (up to 20 per company), choosing the events and whether the webhook covers every project or just one. The same screen shows the secret, the latest deliveries with the receiver's response, the resend button and the test delivery (ping). Creating and changing webhooks isn't available in the API or the MCP.
Events:
issue.created: Task created. From the screen, by e-mail or through MCP.issue.updated: Task edited. Fields, technical log, release, labels, epic, sprint, estimate and due date. Includes what changed.issue.status_changed: Status changed. Including through a retest (passed or failed).issue.assigned: Assignee changed. Assignee or QA owner.comment.created: New comment. Including replies and email replies.automation.triggered: a project automation rule with the "Send webhook" action fired. It goes only to the webhook the rule picked, whether or not that webhook subscribes to the event, and carriesautomation.rule(id,name).ping: the test delivery from the screen, withmessage.
Body (JSON), the same for every event: id (of the event; the same across retries, use it to avoid processing twice), event, created_at, organization (id, slug, name), actor (id, name: who made the change) and issue, a snapshot of the issue at that moment: id, code, title, url, type, status (the workflow key), status_name, status_category (todo, in_progress, done), severity, priority, project (id, name, key), assignee, qa_owner and reporter (id, name, or null), labels, release_id, fixed_version, description, custom_fields, created_at, updated_at and resolved_at. issue.updated, issue.status_changed and issue.assigned carry changes, field by field, {"status": {"from": "open", "to": "doing"}} (the keys of the issue history). comment.created carries comment (id, body in markdown, parent_id when it's a reply, author, mentions and url).
Headers: Content-Type: application/json, User-Agent: tracker-by-aquarela-webhooks/1.0, X-Tracker-Event (the event), X-Tracker-Delivery (the delivery id) and X-Tracker-Signature.
Delivery: public https addresses only (internal networks are refused), without following redirects, with 10 s to respond. Any 2xx counts as delivered; any other response, or none, is retried up to 6 times, after 1 min, 5 min, 30 min, 2 h, 6 h. Respond quickly and process later: arrival order isn't guaranteed.
Signature: X-Tracker-Signature: t=<unix>,v1=<hex>, where v1 is the HMAC-SHA256, in hex, of the text <t>.<raw body> with the webhook's full secret (whsec_...) as the key. Recompute it with the body exactly as it arrived (before parsing the JSON), compare in constant time and reject t more than 5 minutes off. While the secret is being rotated, the header may carry more than one v1: accept it if any of them matches. In 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);
}