Outbound webhooks
Jentrix can POST board events to your HTTPS endpoint — the push half of the agent platform (the pull half is the MCP server). The bundled bug-triage reference agent is a complete working receiver.
Creating a webhook
Workspace Settings → Connections → Webhooks (workspace ADMIN role), or via admin-scoped MCP tools (create_webhook, list_webhooks, delete_webhook, list_webhook_deliveries). A webhook subscribes to one or more events on the workspace's boards; creating one returns a secret used to sign every delivery — store it, it's not shown again.
Events
A webhook is workspace-scoped, so it can subscribe to two families: board events and workspace events. An empty events: [] (or omitting it) delivers all events of both families; otherwise only the named events are delivered.
Board events:
task.created task.updated task.moved task.archived
column.created column.updated column.moved column.archived
board.renamed board.kind_changed
label.created label.updated label.deleted
comment.created comment.updated comment.deleted
attachment.created attachment.removed
deal.updated bug.updated ticket.updated initiative.updated
task.due_soon ticket.sla_breached # system-generated (cron sweep)
automation.fired # from an automation's "fire webhook" action
Workspace events — the harness (coding-harness lifecycle, see agent workflows) family is the headline:
harness.created harness.updated
harness.spec_validated harness.plan_validated
harness.run_started harness.stage_ready harness.stage_started
harness.stage_validated harness.stage_committed
harness.finding_created harness.frozen harness.unfrozen
harness.blocked harness.completed harness.stuck
Other workspace-scoped events (the Agentic OS / delivery layer — e.g. project.*, agent_job.*, release.*, control_tower.item_added) are also subscribable; they fan out through the same workspace channel. Subscribe to the exact names you need, or [] for everything.
Harness deliveries carry compact identifiers plus the workspace id (e.g. harness.stage_committed → { workspaceId, harnessId, stageId }, harness.finding_created → { workspaceId, harnessId, findingId, severity }), plus a fetch hint naming the MCP read tool — never large prompt bodies, diffs, or review text. Fetch the full context over MCP (get_harness, list_harness_findings, …). A receiver that only cares about, say, critical findings can filter on the delivered severity without an extra read.
Delivery format
POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-Stacks-Event: task.created
X-Stacks-Delivery-Id: <unique per delivery>
X-Stacks-Timestamp: <unix epoch seconds>
X-Stacks-Signature: sha256=<hex hmac>
{ "id": "...", "event": "task.created", "createdAt": "...", "data": { ... } }
data carries identifiers (e.g. task.created → { boardId, workspaceId, taskId }; harness events carry workspaceId + harness ids, no boardId), not full entities — fetch what you need through MCP tools. This keeps payloads stable and means a leaked delivery exposes ids, not content.
Verifying signatures
X-Stacks-Timestamp is a Unix timestamp in whole seconds (e.g. 1752192000), not an ISO 8601 string. The signature is an HMAC-SHA256 of the exact string "{timestamp}.{rawBody}" — that same epoch-seconds value, a literal dot, then the raw body — keyed with your webhook secret. Always verify before trusting a delivery, and reject stale timestamps to block replays:
import { createHmac, timingSafeEqual } from "node:crypto";
// Reject deliveries whose timestamp is more than 5 minutes from now.
const MAX_SKEW_SECONDS = 5 * 60;
function verify(secret, headers, rawBody) {
const timestamp = headers["x-stacks-timestamp"] ?? "";
// timestamp is epoch seconds — compare against the current epoch seconds.
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > MAX_SKEW_SECONDS) return false;
const expected =
"sha256=" +
createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(headers["x-stacks-signature"] ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
Use the raw request body bytes — re-serializing parsed JSON breaks the signature.
Retries and auto-disable
- The first delivery attempt happens right after the triggering request finishes.
- Failures retry with exponential backoff, up to 8 attempts (drained by a cron sweep every 5 minutes).
- Respond fast. Deliveries time out after 5 seconds — ack with a 2xx immediately and process async. Use
X-Stacks-Delivery-Idfor idempotency on your side: retries redeliver the same id. - After 3 consecutive deliveries exhaust all retries, the webhook is automatically disabled. Re-enable by deleting and recreating it. Delivery history (pending / success / failed) is visible via
list_webhook_deliveries.
Security rules (SSRF guard)
Destination URLs are validated at creation and before every delivery:
https://only, no credentials in the URL- No localhost, private-range, or link-local destinations — checked after DNS resolution
- Redirects are not followed
For local development against a receiver on your machine, set STACKS_WEBHOOK_ALLOW_PRIVATE=1 in dev only — never in production. To reach a local receiver from a deployed Jentrix, use a tunnel (e.g. cloudflared) that gives you a public HTTPS URL.