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.
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.
{ "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.
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.
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.