API reference
Cases
13 operations in this area of the contract. Every one declares the permission it requires.
Contract version 0.2.0All areas
Operations
/v1/case-queuesConfigured case-routing queues
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
include_retired | query | boolean | — |
Responses
200Tenant case queuesCaseQueue[]
/v1/case-queuesConfigure an organisational case queue
Show parameters and responsesHide parameters and responses
Responses
201Queue createdCaseQueue400—409—
/v1/case-queues/{queueID}Update a live case queue and its priority routes
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
queueIDrequired | path | string | — |
Responses
200Queue updatedCaseQueue404—409—
/v1/case-queues/{queueID}/eligible-assigneesActive tenant users eligible for a queue
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
queueIDrequired | path | string | — |
Responses
200Current eligible users; email addresses are not returned.EligibleCaseAssignee[]404—
/v1/case-queues/{queueID}/retireRetire a case queue without deleting assignment history
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
queueIDrequired | path | string | — |
Responses
200Retired queueCaseQueue404—
/v1/casesInvestigation cases
A metadata-safe caseload register. Each row contains only its opaque id, reference, lifecycle state, priority and opened/SLA/closed times. It never returns the title, narrative summary, subject/customer identity, names, outcome, assignee or actor ids. Those investigation details require `cases.case_content.read` on the case workspace.
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | open | investigating | pending_approval | closed_no_action | closed_reported | reopened | An EXACT match on `case_record.status`, not a category. This list said `[open, in_progress, pending_approval, closed]` until a test walked the request bodies and query parameters as well as the schemas: `in_progress` and `closed` are not values the column has ever held, so a UI dropdown built from this contract had two options that returned an empty page forever and read as "no cases". |
party_id | query | string | Narrows the case list to the cases raised against one party, matched on the case's own subject rather than looked up per row. This is what a customer profile asks for; without it the only way to show one customer's case history is to read the whole case list and filter client-side, which silently goes wrong at the first page boundary. A value that is not a UUID is a 400, not an ignored filter. |
limit | query | integer | Page size, capped at 200. |
cursor | query | string | Opaque keyset cursor from a previous response. Offset pagination is deliberately not offered: `OFFSET 500000` is a table scan, and if it were offered it would be used (ADR-0012). Only the collections that document this parameter can be resumed. The rest report `truncated: true` and refuse a cursor rather than silently returning the first page again — see the `truncated` field on the page schemas. |
Responses
200CasesCasePage
/v1/casesEscalate alerts or screening hits into a case
The case subject is taken from the evidence rather than from the request body, so an analyst cannot open a case against the wrong customer by mistyping. Alerts lead when both kinds are present — an alert names an account and a transaction as well as a party — and a screening hit supplies the subject when there is no alert to take it from.
`alert_ids` and `screening_hit_ids` may name at most 200 records in total. Correlation groups are expanded before any write and the expanded combined set must remain within that boundary; overflow is a 422 and no case is created.
Show parameters and responsesHide parameters and responses
Responses
201The caseCase400—422—
/v1/cases/{caseID}The case workspace
One response distinguishes the immutable evidence linked to this case from the wider history of the same subject. `alerts`, `screening_hits`, `attachments` and `transactions` remain linked filing provenance. Each linked collection is bounded at 200 with an explicit `truncated: true`/`service_cannot_resume` marker; it is never a silent partial array. The four broader subject collections (alerts, transactions, screening runs and screening hits) are independently keyset-resumable investigation context with independent read permissions and explicit coverage. Paging one collection never moves any other collection, and transaction cursors are valid only with the exact half-open window echoed by the response.
Linked `transactions` deliberately remains a bounded, explicitly truncated filing- provenance view. `subject_transaction_history` is the resumable traversal path for the subject's activity inside the selected window. Its database work is a bounded k-way account merge; customers with more than 200 accounts get `unavailable_account_limit` and no misleading partial activity.
A broader-history key is ABSENT when its permission is absent. When permission is present but the case has no usable subject/customer/account, the key is present with an `unavailable_*` or `available_no_accounts` coverage value. An empty `available` list therefore means the query really ran and found no rows; it never means "not allowed".
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
caseIDrequired | path | string | — |
notes_limit | query | integer | Page size for the `notes` sub-collection of the case workspace, capped at 200. |
notes_cursor | query | string | Opaque keyset cursor for the `notes` sub-collection, taken from `notes.next_cursor` on a previous response. Prefixed rather than reusing `cursor` because this response pages TWO children independently. One shared `cursor` would apply a position taken from the timeline to the notes, and the notes would come back from the wrong place under the right key — wrong data presented as right, which is worse than an error. Following it re-reads the case header and the linked alerts as well. That is the stated cost of paging a workspace through its own route instead of adding a second door onto encrypted case notes, whose decryption is a logged PII access. |
events_limit | query | integer | Page size for the `events` sub-collection of the case workspace, capped at 200. |
events_cursor | query | string | Opaque keyset cursor for the `events` (timeline) sub-collection, taken from `events.next_cursor` on a previous response. |
subject_alerts_limit | query | integer | Page size for broader subject alerts, capped at 200. |
subject_alerts_cursor | query | string | Opaque `(opened_at,id)` keyset cursor from `subject_alert_history.next_cursor`. It moves only the alert history; every other case-workspace cursor is independent. |
subject_transactions_limit | query | integer | Page size for broader subject transactions, capped at 200. |
subject_transactions_cursor | query | string | Opaque `(booking_ts,id)` keyset cursor from `subject_transaction_history.next_cursor`. Resubmit the exact echoed `subject_transactions_from` and `subject_transactions_to` values with it. |
subject_transactions_from | query | string | Inclusive RFC3339 start of the broader subject transaction window. Defaults to one calendar year before `subject_transactions_to`; the window may not exceed 366 days. |
subject_transactions_to | query | string | Exclusive RFC3339 end of the broader subject transaction window. Defaults to request time. The response echoes both exact instants so the bounded coverage is reproducible. |
subject_screening_runs_limit | query | integer | Page size for broader subject screening runs, capped at 200. |
subject_screening_runs_cursor | query | string | Opaque `(requested_at,id)` keyset cursor from `subject_screening_history.runs.next_cursor`. |
subject_screening_hits_limit | query | integer | Page size for broader subject screening hits, capped at 200. |
subject_screening_hits_cursor | query | string | Opaque `(created_at,id)` keyset cursor from `subject_screening_history.hits.next_cursor`. |
Responses
200The workspaceCaseDetail400—403—404—
/v1/cases/{caseID}/assignmentRoute a case to a queue or eligible investigator
The queue snapshots its owning organisational unit onto the case. If an assignee is supplied they must currently be active, belong to that exact unit and hold at least one eligible role configured on the queue. Omitting the assignee leaves work owned by the queue without inventing an individual owner.
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
caseIDrequired | path | string | — |
Responses
200Current assignment snapshotCaseAssignment404—409—
/v1/cases/{caseID}/attachmentsAttach immutable investigation evidence
Encrypts the bytes and original filename under the case subject's DEK before writing to the configured write-once object store. The object key contains no case, person, filename or user data. Reusing the UUID `Idempotency-Key` for the same attachment is safe; reusing it for different bytes or metadata is refused.
Legacy `?filename=` query metadata is refused even when `X-Filename` is also present, because silently ignoring it would leave the sensitive value in upstream access logs.
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
caseIDrequired | path | string | — |
Idempotency-Keyrequired | header | string | — |
X-Filenamerequired | header | string | Sensitive display metadata encrypted before storage and never used as an object path. It MUST be redacted from gateway/application logs and MUST NOT be copied into a URL or query string, which access infrastructure commonly records before the handler can redact it. The value must be valid UTF-8, at most 255 bytes, and contain no control character or `/` or `\` path separator. |
Responses
201Immutable encrypted evidence metadataCaseAttachment400—404—409—413Attachment exceeds the 20 MiB evidence limit.415The declared type is unsupported or does not match the bytes.422—
/v1/cases/{caseID}/attachments/{attachmentID}/contentDownload and audit immutable case evidence
Loads the tenant-scoped metadata first, decrypts the opaque object reference and bytes, verifies the stored plaintext SHA-256, and records both PII access and download audit events. The response is always an attachment with `no-store` and `nosniff` headers.
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
caseIDrequired | path | string | — |
attachmentIDrequired | path | string | — |
Responses
200Original verified evidence bytes404—
/v1/cases/{caseID}/closePropose closing a case
This does not close the case. It records a maker-checker proposal and moves the case to `pending_approval`; a different, role-authorised user approves it through `/v1/approvals/{approvalID}/approve`, and the closure is applied inside that approval's transaction.
The two-step flow is the control. Until Phase 2 the closer named their own approver in this request body, with no foreign key and no check that the named person had agreed to anything — the four-eyes rule was self-attested. The proposal payload is digest-bound, so editing the outcome after review invalidates the approval rather than riding on it.
Which roles may approve a case closure is `workflow.approvals` configuration, not code (BRD §7.2).
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
caseIDrequired | path | string | — |
Responses
202The approval request awaiting a second pair of eyesApprovalRequest400—409The case is already closed, or a closure proposal is already pending on it.422—
/v1/cases/{caseID}/notesRecord an investigation note
Notes are encrypted at rest under the subject's key and are append-only. An investigation record that can be edited afterwards is not evidence of anything.
Show parameters and responsesHide parameters and responses
Parameters
| Name | In | Type | Description |
|---|---|---|---|
caseIDrequired | path | string | — |
Responses
201The noteCaseNote400—422—