GET/api/v1/tickets
GET /api/tickets — filtered, priority-ordered list; each row carries its
custom-field values (the private build tracker 2.17) fetched in one batched query.
Pagination is envelope-compatible: the body stays the plain ticket
array; deep-page walks pass the opaque `cursor` query param (from the
previous page's `x-next-cursor` response header) and the server resumes
strictly after that row — keyset, not O(offset) scan (docs/perf.md F2).
Parameters
| Name | In | Type | Required | Description |
|---|
board_id | query | — | No | |
assignee_id | query | — | No | |
client_id | query | — | No | |
category | query | null | TicketStatusCategory | No | |
priority | query | null | Priority | No | |
tags | query | — | No | Comma-separated tag ids (Wave 11 lane 9): AND semantics — tickets
must carry every listed tag. |
limit | query | integer | No | |
offset | query | integer | No | |
cursor | query | — | No | Keyset cursor (Wave 15 lane 3): the opaque `x-next-cursor` value
from the previous page's last row. Resumes the ranked list
strictly after that row — deep pages stop paying O(offset) scan
cost (docs/perf.md F2). When present, `offset` is ignored. |
Responses
Security: bearer
POST/api/v1/tickets
POST /api/tickets — allocates the human ticket number, initializes SLA
clocks, validates + stores any initial custom-field values (REQUIRED
definitions enforced), and emits `ticket.created` — all in one transaction.
Instant-first (IF1/IF2): routed through the shared idempotency gate and
returns the read-your-write `mutation` metadata.
Request body
CreateTicketRequest
Responses
| Status | Description |
|---|
200 | |
409 | version conflict or idempotency key reused |
422 | missing required custom fields or invalid value |
Security: bearer
GET/api/v1/tickets/my-work
GET /api/tickets/my-work — everything assigned to me, not closed,
across all boards, priority-then-SLA-due ordered (the unified
cross-board view from §1; the private build tracker 2.11 adds client/board annotations).
Responses
Security: bearer
GET/api/v1/tickets/{id}
GET /api/tickets/{id} — a conditional read.
The response carries the entity's weak validator (`ETag`) built from its
monotonic write seq, so a caller that still holds that revision sends
`If-None-Match` and gets a bodyless `304 Not Modified` instead of the full
representation. Entities without a counter row behave exactly as before:
no validator, no conditional short-circuit.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
304 | not modified; the If-None-Match validator matched the entity's current revision |
Security: bearer
PATCH/api/v1/tickets/{id}
PATCH /api/tickets/{id} — partial update; validates + upserts any
provided custom-field values; emits `ticket.updated`,
`ticket.field_set` (per changed field), and `ticket.assigned` when the
assignee changes. Instant-first (IF1/IF2): idempotency-keyed, version-
gated, and returns prior + new values for undo.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
PatchTicketRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope (rls_sparse) |
409 | version conflict or idempotency key reused |
422 | invalid custom-field value |
Security: bearer
GET/api/v1/tickets/{id}/checklist
GET /api/tickets/{id}/checklist — lazily instantiates any template items
the ticket has not picked up yet, then returns the full view.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/tickets/{id}/checklist
POST /api/tickets/{id}/checklist — appends an ad-hoc checklist item.
Ad-hoc items never block closure; emits `ticket.checklist_updated`.
Instant-first (IF1/IF2): idempotency-keyed and returns the
read-your-write mutation metadata for the created item.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
AddChecklistItemRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope (rls_sparse) |
409 | idempotency key reused |
Security: bearer
POST/api/v1/tickets/{id}/checklist/complete-all
POST /api/tickets/{id}/checklist/complete-all — the close gate's Option
A batch (docs/notes-tasks-workbar.md §4.2/§5.3): every listed task flips
to `checked` AND lands its server-composed auto-append note, ALL in one
transaction with per-item idempotency keys (`complete-all:{ticket}:{item}`-
derived under the caller's envelope key). Items already terminal
(checked/N-A) are skipped idempotently; an unknown id 404s the whole
batch (atomicity). Every appended note carries
`workflow_meta { kind: "task_closeall", auto: true, item_ids: [..] }` —
the REPORTED auto-close provenance the close-gate dialog displays.
Also usable pre-close, not only at the gate.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
CompleteAllRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket or a listed item not visible in scope (rls_sparse; whole batch atomic) |
409 | idempotency key reused |
Security: bearer
POST/api/v1/tickets/{id}/checklist/{item_id}
POST /api/tickets/{id}/checklist/{item_id} — toggles one checklist item;
emits `ticket.checklist_updated` in the same transaction. Instant-first
(IF1/IF2): idempotency-keyed and returns the read-your-write mutation
metadata for the toggled item.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
item_id | path | string | Yes | |
Request body
SetChecklistItemRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket or item not visible in scope (rls_sparse) |
409 | idempotency key reused |
Security: bearer
GET/api/v1/tickets/{id}/decision-suggestions
GET /api/tickets/{id}/decision-suggestions — the workbench card's
read. Optimistic UI rides the instant-first envelope upstream; this
endpoint is a plain RLS'd read scoped by the ticket gate: the
`tickets:view` permission, then the board gate on the ticket's own
board (403), then the ticket itself (404 outside the org).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
403 | missing tickets:view, or a board denial |
404 | ticket not visible in scope (rls_sparse) |
Security: bearer
GET/api/v1/tickets/{id}/emails
GET /api/tickets/{id}/emails — every ledger row routed to this ticket
(inbound + outbound), newest first, with reply-chain parents resolved
and outbound delivery state projected from the retry ledger.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope |
Security: bearer
GET/api/v1/tickets/{id}/note-draft
GET /api/tickets/{id}/note-draft — restores the caller's autosaved
composer draft (IF2, §5.2). Typed absence (200 with `draft: null`),
never a 404: an absent draft is a normal first-open state, not a miss.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
PUT/api/v1/tickets/{id}/note-draft
PUT /api/tickets/{id}/note-draft — debounced autosave of the workbar
composer draft (NOT a comment; never renders on any ledger). Fresh
drafts ride the IF idempotency-keyed write path and opportunistically
purge 30-day-stale rows.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
PutNoteDraftRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope (rls_sparse) |
Security: bearer
DELETE/api/v1/tickets/{id}/note-draft
DELETE /api/tickets/{id}/note-draft — explicit draft discard.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | draft cleared (idempotent; 200 even with none) |
Security: bearer
POST/api/v1/tickets/{id}/notes/refine
POST /api/tickets/{id}/notes/refine — Tier-2 grammar pass (docs/notes-
tasks-workbar.md §2.5): sends the draft to the org's configured
CompletionProvider (BYOK seam) and returns a PROPOSAL. NEVER a direct
write: the ledger is untouched; the composer shows a diff with
Apply/Discard and Apply rides the normal submit path. Provider
unconfigured/unavailable → typed `status: "unavailable"` + the reason —
the Tier-1 Tidy always works and the UI owns the "not available yet"
state (Template-button pattern).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
RefineNoteRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope (rls_sparse) |
Security: bearer
POST/api/v1/tickets/{id}/revert
POST /api/tickets/{id}/revert — undo one server-accepted edit by
restoring its prior values as a NORMAL authorized idempotent write
(IF1 envelope end to end). Appends a new trail row (op `revert`),
emits `ticket.reverted` on the event bus, and writes the audited
`ticket.reverted` trail row — the append-only history is never
rewritten.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
RevertTicketRequest
Responses
| Status | Description |
|---|
200 | |
409 | version conflict, idempotency key reuse, or undo_reverts_only (closed record, whole-record revert) |
422 | unknown target seq, non-restorable step/field, or nothing to restore |
Security: bearer
GET/api/v1/tickets/{id}/similar-tickets
GET /api/tickets/{id}/similar-tickets — retrieval top-k + per-candidate
relatedness Noul. Degrade-never-5xx: a quiet decision edge returns the
plain retrieval list, never an error.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
q | query | — | No | Override the retrieval text (defaults to the ticket's own title
plus a body excerpt). Clamped like the search surface. |
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope (rls_sparse) |
Security: bearer
GET/api/v1/tickets/{id}/sla
GET /api/tickets/{id}/comments — optional kind filter; internal notes
require ticket view. The portal surface must consume the dedicated
client-visible read (`core-db::tickets::list_client_visible_comments`),
never this staff endpoint.
GET /api/tickets/{id}/sla — clock state plus a live business-time
remaining estimate for the resolution budget (drives the UI badge).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/tickets/{id}/status
POST /api/tickets/{id}/status — the guarded lifecycle transition:
domain transition rules, pre-close checklist enforcement (typed 409
listing the incomplete required items when the board enforces), SLA
pause bookkeeping, and the `ticket.status_changed` event, all in one tx.
Instant-first (IF1/IF2): idempotency-keyed, version-gated, read-your-write.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
ChangeStatusRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope (rls_sparse) |
409 | pre-close checklist blocked, version conflict, or idempotency key reused |
422 | invalid transition |
Security: bearer
GET/api/v1/tickets/{id}/time-entries
GET /api/tickets/{id}/time-entries
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
GET/api/v1/tickets/{id}/undo-stack
GET /api/tickets/{id}/undo-stack — the per-field / per-record
applied-state history read model for one record.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
limit | query | — | No | Maximum steps / field rows returned (1–200, default 50). Newest
first; the first step's `seq` is the record's current version. |
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope |
Security: bearer