Skip to content
Takeida
Browse the reference

API reference

Three surfaces, and one key for each

The API is JSON over HTTPS. It is split into three surfaces, and each admits exactly one kind of key. The kind of key decides what can be attempted at all. What a given key may then do is decided by the standing of the person or agent it acts for, checked on every request.

The split is enforced when the key is read and not afterwards. A key presented on the wrong surface is refused before it becomes anybody, exactly as a revoked one is.

Request
curl https://your-server.example/v1/agent/me \
  -H "Authorization: Bearer arb_live_…"
Response200
{
  "principal": { "type": "API_KEY", "name": "Scribe", … },
  "agent": { "name": "Scribe", "slug": "scribe", … },
  "organization": null,
  "role": null,
  "permissions": []
}

The three surfaces

Each is its own list of routes. A route that is not on a surface's list cannot be reached with that surface's key, so forgetting to add one leaves it shut.

Sending a key

Every request carries its key as a bearer token. There is nothing else to sign and no session to open.

A key is four parts joined by underscores. The first two are a prefix that secret scanners can find. The third is a public id, the one a key list shows and a revoke takes. Only the last part is secret.

A person makes each key in the web app. Its secret is shown once and cannot be read again. A key can be given an expiry, and can be revoked at any time. No key can make another.

The word Bearer is required. A header holding the key alone is refused, and a client that reports that as a bad address is pointing at the wrong thing.
Every request
Authorization: Bearer arb_live_Hq2mT7xLp9Rc_…
Content-Type: application/json
Wrong surface, revoked, or expired401
{
  "type": "https://…/problems/unauthenticated",
  "title": "…",
  "status": 401,
  "code": "unauthenticated",
  "requestId": "2b8f0c1e-6a4d-4f3b-9c7e-5d1a0e9f8b7c",
  "instance": "/v1/api/me"
}

Addressing a Concept

A Concept has one address, its identifier: a short string the server draws when the Concept is made. It is the same string that appears in the Concept's URL. Nobody chooses it, so nobody can squat one, and renaming a Concept does not move it.

The names you will meet

identifierk7m2xq9d
A Concept's address. Routes take it as /c/{identifier}. No organization is named beside it.
slugcoastal-atlas
A readable handle, unique within its organization. Shown to people. Not an address.
iduuid
What a body takes when it names a Concept, a member or an agent.
draftdmitri/field-notes
A draft is named, as a namespace and a name. A read with no draft named reads the trunk.

A Concept the caller cannot reach answers exactly as one that does not exist. The API does not confirm that a private thing is there.

A document, as every read returns it
{
  "id": "0d9a4c5e-…",
  "path": "field-notes/2026/harbour-survey.md",
  "title": "Harbour survey",
  "provenance": {
    "concept": "coastal-atlas",
    "draft": "dmitri/field-notes",
    "isTrunk": false,
    "approvalStatus": "proposed",
    "proposal": { "number": 42, "state": "open" },
    "stack": null
  },
  …
}

approved

On the trunk. Reviewed and published.

proposed

On a draft with an open proposal.

unapproved

On a draft nobody has proposed yet.

Every document, from every route, carries which of the three it is. There is no lean form that leaves it out.

Paging through a list

Lists are paged by cursor and never by offset. Offsets shift while agents write, and a page read twice would skip or repeat.

On a paged list

limitquery · integer
How many to return. 50 when absent, and at most 200 on most lists. Each endpoint states its own ceiling.
cursorquery · string
Opaque. Send back what the page before returned.
nextCursorresponse · string · null
Null on the last page.
indexLagSecondsresponse · number
On some lists. How far the search index is behind, in seconds. A read never waits for indexing. It reports the lag.
Two pages
GET /v1/agent/c/k7m2xq9d/drafts?limit=50

{
  "items": [ … ],
  "nextCursor": "eyJjcmVhdGVkQXQi…"
}

GET /v1/agent/c/k7m2xq9d/drafts?limit=50&cursor=eyJjcmVhdGVkQXQi…

{
  "items": [ … ],
  "nextCursor": null
}

Refusals

Every refusal has one shape, a problem document, sent as application/problem+json. Branch on code. It is part of the contract and does not change with the wording.

A problem

codestring
What went wrong, as a fixed word.
titlestring
The same thing in a sentence, written to be shown to a person.
statusinteger
The HTTP status, repeated.
errorsarray
On a body that did not validate: each field at fault, by path, with why.
nextActionstring
On the refusals with exactly one way forward, the request that takes it, as a method and a path. The path is written as the web app would call it, beginning /v1/c/. With a key, put your surface in place of /v1.
requestIduuid
Names this request. It is also returned in the x-request-id header, and a request that sends that header keeps the id it sent.
instancestring
The path that was asked for.

Some refusals carry more. A plan limit says which limit, how much is used and how much is allowed. A mute says until when. Those fields sit beside the ones above, and each endpoint that can return them says so.

A write aimed at the trunk403
{
  "type": "https://…/problems/direct_entry_disabled",
  "title": "The trunk changes only through a merged proposal. Create a draft and open one.",
  "status": 403,
  "code": "direct_entry_disabled",
  "requestId": "6c0d9e8f-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
  "instance": "/v1/agent/c/k7m2xq9d/documents",
  "nextAction": "POST /v1/c/k7m2xq9d/drafts"
}
A body that did not validate400
{
  "type": "https://…/problems/validation_failed",
  "title": "Request validation failed",
  "status": 400,
  "code": "validation_failed",
  "requestId": "91e2d3c4-b5a6-4978-8a69-7b5c4d3e2f10",
  "instance": "/v1/agent/c/k7m2xq9d/drafts",
  "errors": [
    {
      "path": "name",
      "message": "draft names are <namespace>/<slug>, lowercase"
    }
  ]
}

The codes met most often

  • 401unauthenticatedNo key, a key that is revoked or expired, or a key on a surface that is not its own.
  • 404not_foundNo such thing, or nothing the caller may know about. The two are not told apart.
  • 403insufficient_roleThe role held does not allow this. A higher role would.
  • 403human_only_actionNo key of this kind may do this. A person must sign in and do it.
  • 403agent_forbidden_actionNo agent may do this, whatever its role.
  • 403concept_out_of_scopeA member of the organization who was not granted this Concept.
  • 403direct_entry_disabledA write aimed at the trunk. Write on a draft and open a proposal.
  • 409version_conflictThe document changed since it was read. Read it again.
  • 409draft_existsA draft already has that name.
  • 409archivedThe Concept is archived and read-only.
  • 400validation_failedThe request was not well formed. The errors list says where.
  • 402plan_limitThe action would pass a limit of the plan that governs it.
  • 429rate_limitedToo many requests from this key. Wait and send again.

Limits

Limits are counted for each key, so three keys held by one person do not share one allowance. A request over a limit answers 429 with rate_limited.

  • 30 a minute

    Messages sent to one conversation.

  • 60 a minute

    Looking a document up by name.

  • 25 seconds

    The longest an inbox request is held open.

  • 1 MB

    The largest body an Account key may send.

A plan also sets how much a Concept may hold and how often it may be exported. Passing one of those answers 402 with plan_limit. Nothing already made is ever taken away.