Skip to content

API reference · conventions

What every request shares

Six rules, and every operation page assumes them. The long form is api-guidelines.md; this is the short one, with the shapes the contract declares.

Authentication

One scheme, bearerAuth: an Authorization: Bearer header carrying an OIDC access token, a personal access token (hbt_pat_…) or a service account token. Which of the three it is makes no difference to an operation — a token is refused precisely what the person or account behind it would be refused. The few routes without it say so on their own page.

A bearer http
GET https://{host}/api/v1/accounts/meAuthorization: Bearer hbt_pat_…

A refusal is a problem document

Every error is RFC 9457 with a stable code that is part of the contract, a detail_code that names a message in the catalogue, the parameters that message takes, and one entry per field where a field was wrong. There is no English in it: the client owns every sentence.

NameTypeRequiredDescription
typestring (uri) optional
titlestring optional
statusinteger required
codestring requiredA stable, machine-readable error code (part of the contract).
detail_codestring optionalThe message code for localisation in the client.
paramsobject optional
field_errorsarray of object optional
request_idstring optional
docsstring (uri) optional
A 422 json
{  "type": "about:blank",  "title": "Unprocessable Entity",  "status": 422,  "code": "validation_failed",  "detail_code": "items.title_too_long",  "params": { "max": 500 },  "field_errors": [    { "path": "title", "code": "items.title_too_long", "params": { "max": 500 } }  ],  "request_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c"}

Idempotency

Every mutation takes an Idempotency-Key, a UUID the client chooses. The same request under the same key takes effect once and is answered from the stored response for 24 hours — which is what makes a retry after a lost connection safe. Two answers are deliberately not kept: a 5xx, and a refusal that asks for a step-up proof, because neither is an outcome of the request.

Concurrency

Every resource answers an ETag and carries a version. A partial write sends the ETag it last read as If-Match; a write against a state somebody else has moved on is refused with 412 and version_conflict, and the client reads again rather than overwriting what it never saw. Partial writes are application/merge-patch+json: an absent key changes nothing, null clears.

A partial write application/merge-patch+json http
PATCH https://{host}/api/v1/items/01J9Z1Q4K7Content-Type: application/merge-patch+jsonIf-Match: "3"{  "title": "Move the socket by the window",  "due_at": null}

Line 3 is the version this write is against. Line 6 sets a field; line 7 clears one, which is what null means here and is the only way to clear it. Every field not in the body — the bucket, the labels, the assignee — is left exactly as it was.

Pagination is a cursor

A listing answers a page and a next_cursor; the next request sends it back. There are no page numbers anywhere, and a cursor is opaque and signed — a client can neither construct one nor read one. A listing whose reach is checked per record can answer a short page that is not the last: walk on until has_more is false.

Two pages http
GET https://{host}/api/v1/items?collection_id=…&limit=50→ { "items": [ … ], "next_cursor": "eyJ…", "has_more": true }GET https://{host}/api/v1/items?collection_id=…&limit=50&cursor=eyJ…

Actions are a suffix

Something that is neither a read nor a field change is a POST to the resource with a colon suffix: /items/{itemId}:complete, /backups/{backupId}:verify. An action that cannot be bounded answers 202 and a job, and /jobs/{jobId} is where its progress and its result are read.