REST API

Truke KF exposes a small REST/JSON API under /api. With it you can read an item, run a full-text search, walk the relations between items, and create or update items and their related objects (events, actions, components, and so on) — everything the web interface does with the item store, from a script or an integration.

The API carries exactly your own access: a request made with your token sees the same items you would see in the browser, no more. See authentication & access for how identity and access labels work.

Interactive documentation

The full specification is browsable and testable in your browser:

AddressWhat it is
/api/docsSwagger UI — try every endpoint interactively
/api/openapi.yamlthe raw OpenAPI 3.0 document

Open /api/docs, click Authorize, paste a token (below), and you can call any endpoint directly from the page.

Authentication

Every data endpoint requires a personal access token sent as a bearer credential:

Authorization: Bearer 

Only /api/health, /api/docs and /api/openapi.yaml are public — everything else returns 401 without a valid token.

Create a token (you must be logged in). The plaintext is shown once — store it immediately, it cannot be retrieved later:

curl -X POST https://kf.example.com/auth/tokens \
  -d name=my-script -d ttl=720h
# {"id":"a1b2c3d4","token":"f0e1d2…"}

name is a label for your own reference; ttl is an optional lifetime (for example 720h for 30 days) — omit it for a token that never expires. Revoke a token with POST /auth/tokens/{id}/revoke. See access tokens for the full details.

How long is a token valid? As long as you decide. Unlike the browser

session (which expires after a fixed period, 24 hours by default), an API

token has no default and no maximum lifetime — it lives for exactly the ttl

you set at creation, or forever if you omit it. Once issued, the only way to end

it early is to revoke it.

ttl must use Go duration units — h (hours), m (minutes), s (seconds); for

example 720h, not 30d. A value KF cannot parse is treated as no expiry, so

a typo like 30d silently produces a token that never expires.

Conventions

  • All request and response bodies are JSON; send Content-Type: application/json on POST and PUT.
  • An item is identified by its id (or uid). Items also have an optional human code (an SKU or x0 number) and a space-separated list of tags.
  • Every item has a class: m (module), i (item), e (event), a (action), t (type). The class controls how an item behaves and how it is shown.
  • Errors are returned as { "error": "…" } with an appropriate HTTP status (400 invalid input, 401 no/invalid token, 403 not permitted, 404 not found, 409 a code collides with a sibling's).
  • An unauthorized read is answered 404, not 403. Telling a caller that an item exists but is off limits is itself a disclosure. Writes answer 403, because by then the caller has already demonstrated they know what they are aiming at.
  • List endpoints paginate with limit and offset, and return X-Total-Count and X-Offset. The total is counted after access filtering, so it is what you may see, not what exists.

Endpoints

Method & pathPurpose
GET /api/healthLiveness probe
GET /api/provenanceWhat this database is — build, file, item/edge/revision counts, last write, configuration revision
GET /api/audit-trailWhat this reader examined
GET /api/listEnumerate a set of items
GET /api/searchFull-text (BM25) search
POST /api/itemsCreate an item
GET /api/items/{id}Get an item by id
PATCH /api/items/{id}Change any field of an item
PUT /api/items/{id}Update an item (legacy; prefer PATCH)
DELETE /api/items/{id}Trash an item, or delete it outright
GET /api/items/by-code/{code}Get an item by its code (x0 / SKU)
GET /api/items/by-tag/{tag}List items carrying a tag
GET /api/items/{id}/pathBreadcrumb path of an item
PUT /api/items/{id}/parentMove an item to a different parent
POST /api/items/{id}/copyCopy a subtree under this item
GET /api/items/{id}/historyEvery revision of an item, newest first
GET /api/items/{id}/relatedEvery item connected to this one, over every kind of edge
GET /api/items/{id}/checklistWhat this item still owes
POST /api/items/{id}/checklist/materialise-allTake ownership of every inherited requirement at once
POST /api/items/{id}/checklist/{checkId}Take ownership of one inherited requirement
GET /api/items/{id}/{relation}List related items
POST /api/items/{id}/{relation}Link an item to this one, creating it if it does not exist
DELETE /api/items/{id}/{relation}/{otherId}Remove the edge between two items

This table is checked against the router by a test, so a route and the specification cannot disagree.

PATCH — change any field

The store is schemaless: an item carries whatever fields it was given. An API built around a fixed struct therefore cannot reach most of what KF holds — risk inputs, schedule dates, declared attributes — and for a while this one could not.

PATCH takes any field:

# set the risk inputs on an event
curl -X PATCH https://kf.example.com/api/items/$ID \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"severity":"S3","occurrence":"E4","controllability":"C3"}'

# give an action a duration and a completion figure
curl -X PATCH … -d '{"td":"6w","progress":60}'

Status is a tag, so marking something done is the same call:

curl -X PATCH … -d '{"tags":{"add":["done"]}}'

Linking two items that already exist

POST /api/items/{id}/{relation} has two shapes, and they are different requests:

  • {"id": "…"} links an item that already exists. This is the only way to write an edge between two existing nodes.
  • A description (title and friends) creates a node and links it.

A body carrying both is refused. The relation implies a default class, which class overrides — which matters for tasks: a task defaults to an action, but may equally be an object or a document, since what makes it a task is the edge and not its class.

Relations are named for the role the neighbour plays, in both directions:

effects/causes, components/part_of, tasks/task_of,

instances/types, failure_modes/failure_mode_of,

followed_by/preceded_by, links_to/linked_from, plus events and sidebar.

Closing a checklist gap in one call

POST /api/items/{id}/checklist/{checkId} takes ownership of a requirement inherited from a type — the instance stops merely owing it and starts holding it.

materialise-all does the same for every inherited requirement at once, which is how a template becomes a project.

GET /api/search?q=pump+failure

Full-text search over all items, in BM25 rank order (best match first). The query uses FTS5 syntax: plain keywords, "exact phrases", prefix* wildcards, and the boolean operators AND / OR / NOT.

curl -G https://kf.example.com/api/search \
  --data-urlencode "q=pump AND (seal OR bearing)" \
  -H "Authorization: Bearer $TOKEN"

Each result is a compact reference:

[
  { "id": "3k9", "title": "Pump seal leak", "type": "e", "link": "/3k9" }
]

Read an item

GET /api/items/{id} returns the full item:

{
  "id": "3k9",
  "uid": "…",
  "title": "Centrifugal pump",
  "class": "i",
  "code": "PMP-100",
  "tags": "critical rotating",
  "parent": "1a2",
  "doc": "Markdown body of the item…"
}

You can also fetch by code (GET /api/items/by-code/PMP-100) or list every item carrying a tag (GET /api/items/by-tag/critical). GET /api/items/{id}/path returns the breadcrumb trail:

{ "id": "3k9", "path": "Home / Machine / Pump" }

GET /api/items/{id}/{relation} lists the items linked to an item by a given relation. {relation} is one of:

RelationDirectionWhat it lists
eventsdownfailure modes / problems annotated on the item
componentsdownsub-parts (bill of materials)
tasksdownto-do tasks — actions to perform, objects or documents to produce
instancesdownitems that draw from this one as a type
sidebardownsidebar navigation elements
causesupcauses leading to this event
typesuptypes this item learns from
curl https://kf.example.com/api/items/3k9/events \
  -H "Authorization: Bearer $TOKEN"

Create and update items

Create an item with POST /api/items. Only title is required; class,

code, tags, doc and parent are optional:

curl -X POST https://kf.example.com/api/items \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Centrifugal pump","class":"i","code":"PMP-100","tags":"critical"}'

The response is the created item (HTTP 201), including its new id.

Update with PUT /api/items/{id}. Only the fields you send (title, doc,

code, tags) are changed; anything omitted is left as it was.

POST /api/items/{id}/{relation} creates a new item of the class implied by the relation and links it to {id} in one step. For example, to annotate a failure event on a component:

curl -X POST https://kf.example.com/api/items/3k9/events \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Seal leak under load"}'

The down relations (events, components, tasks, instances, sidebar) attach the new item below {id}; the up relations (causes, types) attach it above. The body is the same NewItem shape as POST /api/items;

parent is ignored — the relation decides it.

Each relation implies a default class, but class in the body overrides it. This is how you choose what kind of task you are creating: POST /api/items/{id}/tasks defaults to class a (an action to perform), while passing "class":"doc" creates a document that has to be produced. Either way the item is linked as a task of {id}.

A minimal session

export TOKEN=f0e1d2…

# find an item
curl -G https://kf.example.com/api/search --data-urlencode "q=pump" \
  -H "Authorization: Bearer $TOKEN"

# read it
curl https://kf.example.com/api/items/3k9 -H "Authorization: Bearer $TOKEN"

# annotate a failure event on it
curl -X POST https://kf.example.com/api/items/3k9/events \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"title":"Bearing seizure"}'