Delivery Trace
One ordered timeline of how a ticket actually got delivered -- claims, reports, comments, status moves, the branch, the PR, pipelines and booked time.
The delivery trace answers "how did this ticket get done, and who did what" in one ordered list. It aggregates records other modules already own — it stores nothing of its own and changes nothing.
Get the Trace
GET /api/v1/boards/{boardId}/tickets/{ticketId}/delivery-traceRequires the tickets:view permission and access to the board. Accepts an agent's access token, so a loop can read the story before deciding whether to start.
Customers cannot read it — 403, by role, not by filtering the payload. The trace is engineering process (which machine held the ticket, how long its lease ran, how many minutes are still drafts); customers see time through the deliberately coarser customer overview.
Example Response
{
"ticketId": "tkt_abc123",
"displayId": "WEB-42",
"branch": "feature/web-42-login-styling",
"items": [
{
"at": "2026-09-05T08:00:00Z",
"kind": "claimed",
"label": "Claimed for 30 minutes via claude-code",
"actor": {
"id": "usr_agent123",
"name": "Nightly Refactor",
"isAgent": true,
"clientName": "claude-code"
},
"ref": null,
"refType": null
},
{
"at": "2026-09-05T08:26:00Z",
"kind": "reported",
"label": "Reported needs_review",
"actor": { "id": "usr_agent123", "name": "Nightly Refactor", "isAgent": true, "clientName": "claude-code" },
"ref": "https://github.com/acme/web/pull/312",
"refType": "url"
}
],
"gaps": ["coding_tool_handoff"]
}Fields
| Field | Type | Description |
|---|---|---|
| ticketId | string | The ticket |
| displayId | string | null | Speaking key, e.g. WEB-42 |
| branch | string | null | Source branch of the linked pull request, if there is one |
| items | object[] | Ascending by at |
| gaps | string[] | Sources not represented in this trace, so a reader can tell "did not happen" from "not recorded" |
Item
| Field | Type | Description |
|---|---|---|
| at | string | ISO 8601 timestamp |
| kind | string | See the list below. Treat an unknown kind as "render the label" — the list grows |
| label | string | Short, already-composed English label. Machine output, not translated |
| actor.id | string | null | Spedy user id; null for actors known only by name (a git author, the system) |
| actor.name | string | Display name, or System when nothing acted |
| actor.isAgent | boolean | Whether the actor is an agent user |
| actor.clientName | string | null | The MCP client the action came through, e.g. claude-code. null for the web UI |
| ref | string | null | What the item points at: an absolute URL, a ticket key, a branch name, a status name |
| refType | "url" | "key" | null | How to treat ref |
Kinds
claimed · heartbeat · claim_released · claim_expired · reported · first_agent_comment · first_human_comment · status_changed · branch · pr_opened · pr_review · pr_merged · pipeline · time_entry
Time entries appear including drafts, marked as drafts. Claims carry the client they were taken through, which is why a claim row can read "via claude-code" rather than just naming the actor.
Gaps
gaps is the honest part. A trace that simply omitted an unrecorded source would look identical to a trace where nothing happened, and it would silently start leaking the next time a source is added. Anything Spedy does not record at all is named here instead — today that always includes coding_tool_handoff.
In the product
The same timeline is the collapsible Delivery trace section on a ticket's Activity tab, and the same data drives the Agents overview. Over MCP a loop reads it with the tickets_delivery_trace tool.