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.
The full specification is browsable and testable in your browser:
| Address | What it is |
|---|---|
/api/docs | Swagger UI — try every endpoint interactively |
/api/openapi.yaml | the raw OpenAPI 3.0 document |
Open /api/docs, click Authorize, paste a token (below), and you can call any
endpoint directly from the page.
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.
Content-Type: application/json
on POST and PUT.id (or uid). Items also have an optional
human code (an SKU or x0 number) and a space-separated list of tags.m (module), i (item), e (event), a (action),
t (type). The class controls how an item behaves and how it is shown.{ "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).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.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.| Method & path | Purpose |
|---|---|
GET /api/health | Liveness probe |
GET /api/provenance | What this database is — build, file, item/edge/revision counts, last write, configuration revision |
GET /api/audit-trail | What this reader examined |
GET /api/list | Enumerate a set of items |
GET /api/search | Full-text (BM25) search |
POST /api/items | Create 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}/path | Breadcrumb path of an item |
PUT /api/items/{id}/parent | Move an item to a different parent |
POST /api/items/{id}/copy | Copy a subtree under this item |
GET /api/items/{id}/history | Every revision of an item, newest first |
GET /api/items/{id}/related | Every item connected to this one, over every kind of edge |
GET /api/items/{id}/checklist | What this item still owes |
POST /api/items/{id}/checklist/materialise-all | Take 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 fieldThe 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"]}}'
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.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.
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" }
]
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:
| Relation | Direction | What it lists |
|---|---|---|
events | down | failure modes / problems annotated on the item |
components | down | sub-parts (bill of materials) |
tasks | down | to-do tasks — actions to perform, objects or documents to produce |
instances | down | items that draw from this one as a type |
sidebar | down | sidebar navigation elements |
causes | up | causes leading to this event |
types | up | types this item learns from |
curl https://kf.example.com/api/items/3k9/events \ -H "Authorization: Bearer $TOKEN"
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}.
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"}'