For Developers
{
"version": 1,
"type": "task.moved",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": {
"taskId": "task_ghi789",
"taskTitle": "Fix login redirect",
"fromColumnId": "col_todo",
"toColumnId": "col_in_progress",
"fromColumn": "To Do",
"toColumn": "In Progress",
"fromCategory": "todo",
"toCategory": "in_progress"
}
}Create a webhook under a project's Settings → Webhooks, pick an HTTPS endpoint, and select which of the 40 event types below to receive. Every delivery is a POST with a version field (currently 1) so you can detect a future breaking payload change, and an X-Signature: sha256=... header — an HMAC-SHA256 of the raw body using your webhook's secret — so you can verify the request really came from UnityFreak.
task.createdtask.updatedtask.movedtask.assignedtask.unassignedtask.completedtask.archivedtask.unarchivedtask.deletedtask.restoredtask.due_soontask.overdueA task.created from an inbound email or a client ticket submission carries only { taskId, title, source } — no assignee fields, since it has no assignee yet. A task.completed triggered by an automation or git transition (rather than a person) omits the assignee fields too.
{
"version": 1,
"type": "task.moved",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": {
"taskId": "task_ghi789",
"taskTitle": "Fix login redirect",
"fromColumnId": "col_todo",
"toColumnId": "col_in_progress",
"fromColumn": "To Do",
"toColumn": "In Progress",
"fromCategory": "todo",
"toCategory": "in_progress"
}
}task.comment.addedtask.comment.mentionedA reply to a client ticket-intake thread also fires task.comment.added, but with a smaller payload: { taskId, source, taskTitle, assigneeId, assigneeIds } — no commentId, mentionedUserIds, or assignees.
{
"version": 1,
"type": "task.comment.added",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": {
"taskId": "task_ghi789",
"commentId": "cmt_jkl012",
"taskTitle": "Fix login redirect",
"mentionedUserIds": [],
"assigneeId": "usr_mno345",
"assigneeIds": ["usr_mno345"],
"assignees": [{ "id": "usr_mno345", "username": "ana", "displayName": "Ana" }]
}
}task.custom_field.user_added{
"version": 1,
"type": "task.custom_field.user_added",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": {
"taskId": "task_ghi789",
"taskTitle": "Fix login redirect",
"fieldId": "cf_severity",
"fieldName": "Reviewers",
"addedUserIds": ["usr_mno345"]
}
}sprint.createdsprint.startedsprint.completedsprint.revertedsprint.reopened{
"version": 1,
"type": "sprint.completed",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": {
"sprintId": "spr_pqr678",
"title": "Sprint 14",
"kind": "sprint",
"projectId": "proj_abc123",
"totalTasks": 24,
"completedTasks": 21,
"rolledOverTasks": 3,
"targetSprintId": "spr_stu901",
"source": "manual"
}
}epic.createdepic.updatedmilestone.createdmilestone.missedmilestone.missed is sent by the daily milestone check, with actorId "system", when a milestone's due date has passed and it still has open tasks. Its payload is { milestoneId, milestoneName, undoneTaskCount }.
{
"version": 1,
"type": "epic.created",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": { "epicId": "epc_vwx234", "title": "Q4 billing overhaul" }
}dependency.createddependency.overriddendependency.reinstateddependency.deleted{
"version": 1,
"type": "dependency.created",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": {
"dependencyId": "dep_yza567",
"blockingTaskId": "task_ghi789",
"blockedTaskId": "task_bcd890"
}
}approval.requestedapproval.completedapproval.rejectedA manually requested approval carries policy ("any" or "all"). An approval requested automatically by a workflow column gate carries gateType ("entry" or "exit") instead — a delivery has exactly one of the two, never both.
{
"version": 1,
"type": "approval.requested",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": {
"approvalId": "apr_efg123",
"taskId": "task_ghi789",
"taskTitle": "Fix login redirect",
"approverIds": ["usr_mno345"],
"policy": "any"
}
}sla.warningsla.breached{
"version": 1,
"type": "sla.breached",
"projectId": "proj_abc123",
"actorId": "system",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": {
"trackerId": "slt_hij456",
"taskId": "task_ghi789",
"taskTitle": "Fix login redirect",
"policyId": "slp_klm789",
"targetHours": 24,
"elapsedHours": 26.3
}
}task.git.commit_linkedtask.git.pr_linked{
"version": 1,
"type": "task.git.pr_linked",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": {
"taskId": "task_ghi789",
"prNumber": 42,
"repoFullName": "acme/web",
"title": "Fix login redirect"
}
}project.imported{
"version": 1,
"type": "project.imported",
"projectId": "proj_abc123",
"actorId": "usr_def456",
"timestamp": "2026-09-11T14:22:00.000Z",
"payload": { "format": "jira", "imported": 118, "skipped": 2, "total": 120 }
}Also subscribable, not shown above with a full example:
milestone.updatedmilestone.deletedsprint.updatedsprint.deletedconst crypto = require('crypto');
function isValid(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}If you cannot host an HTTPS receiver, the PM API covers the same ground by polling: GET /tasks with an updatedSince checkpoint returns everything that changed across every project a key can see.
A 429 or 5xx response is retried with exponential backoff, up to 8 attempts or 24 hours after the original event, whichever comes first. Any other non-2xx response (for example 400 or 401) is treated as a permanent failure and is not retried. There is currently no automatic disable after repeated failures — check a delivery's status from the same settings page if you suspect an endpoint has gone dark.