🧪 killBottleneck is in public beta — cloud and self-host.🧪 killBottleneck is in beta.Beta on GitHub →
Skip to content

REST API

A small, deliberately narrow API for reading and editing maps and tasks from your own code. It is the same surface the MCP server is built on.

Looking for a worked example?

Connecting Google Sheets is the whole recipe step by step — code for Apps Script and for n8n, conflict handling (409) included.

  • Works for the cloud and self-hosted alike — a hosted instance (yourcompany.killbottleneck.com) exposes the same API as one on your server; only the address differs
  • Base path: /api/kb/v1 — the older /api/flowmap/v1 prefix still resolves to the same handlers
  • Authentication: an API key in the Authorization header. Never a session cookie or JWT
  • Format: JSON in, JSON out
  • Language: English only, by design

Authentication

Create a key in the application, under your user menu. The plaintext token is shown once — only its SHA-256 hash is stored, so a lost key cannot be recovered, only rotated.

Authorization: Bearer kb_user_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Keys issued before the rename start with fm_user_ and keep working.

Scopes

ScopeMay call
readevery GET endpoint
read_writeeverything, including all POST endpoints

A read key calling a write endpoint gets 403. The default when creating a key is read.

What a key can never do

  • The owner comes from the key, never from the request body. A key only reaches maps and tasks belonging to the account that created it.
  • A key cannot escalate. The server never establishes a session for it and never reads the account's role, so an API key cannot do administrator things.
  • Someone else's map id returns 404, not 403 — the API does not confirm that an id exists.

Limits

LimitValue
Requests per minute, read120 per key
Requests per minute, read_write30 per key
Request body2 MB
Items per call (nodes)200
Keys per account20

Read and write are counted in separate buckets, so bulk reading cannot starve your writes. Over the limit you get 429.

Optimistic locking

Every endpoint that changes an existing map requires base_updated — the updated value you got when you last read the map.

GET  /api/kb/v1/maps/{id}        →  { "updated": "2026-07-30 08:12:44.031Z", … }
POST /api/kb/v1/maps/{id}/nodes  ←  { "base_updated": "2026-07-30 08:12:44.031Z", … }

Omit it and you get 400. Send a stale one and you get 409 — someone changed the map in the meantime. Re-read the map, re-apply your change, try again.

This is deliberate: it makes "write without reading first" impossible, which is exactly the mistake an over-eager AI assistant makes.

Endpoints

GET /api/kb/v1/maps

Lists maps owned by the key's account.

QueryMeaning
archived=1list archived maps instead of active ones
json
{
  "maps": [
    { "id": "abc123", "title": "Website relaunch", "node_count": 14, "updated": "2026-07-30 08:12:44.031Z" }
  ]
}

GET /api/kb/v1/maps/{id}

One map as a tree. No canvas positions — the shape, not the drawing.

json
{
  "id": "abc123",
  "title": "Website relaunch",
  "description": "",
  "archived": false,
  "updated": "2026-07-30 08:12:44.031Z",
  "tree": [ { "id": "n_1", "title": "Content", "status": "todo", "children": [] } ],
  "notes": []
}

Keep the updated value — you need it as base_updated for any write.

POST /api/kb/v1/maps

Creates a map from an outline. Requires read_write.

FieldRequiredMeaning
titleyesMap title, trimmed to 200 characters
treenoArray of items (see below); max 200 nodes in total
descriptionnoFree text
apex_textnoText of the root goal; defaults to title

Each item in tree may carry title, description, deadline, owner, status, wait_for_children and children. Layout is computed for you.

Returns { id, title, updated, tree }.

POST /api/kb/v1/maps/{id}/nodes

Adds a subtree. Requires read_write.

FieldRequiredMeaning
base_updatedyesVersion you read (see optimistic locking)
itemsyesArray of items, at least one, max 200
parent_idnoWhere to attach; omitted = under the apex

Returns { updated, added_ids, tree }.

This re-computes the layout of the whole map

Adding nodes rearranges positions across the map. It is not a surgical insert.

POST /api/kb/v1/maps/{id}/nodes/{nodeId}

Updates one node. Requires read_write and base_updated.

Settable fields: title, status (todo / in_progress / done), description, deadline (YYYY-MM-DD, empty string clears), owner (e-mail, empty string clears), wait_for_children, color, executor_kind, executor_name, automation_wanted, automation_note.

Setting status to done has side effects on purpose: it may unblock waiting nodes, notify their owners, and trigger automations attached to them.

POST /api/kb/v1/maps/{id}/nodes/{nodeId}/delete

Deletes a node including its whole subtree. Requires read_write and base_updated.

The apex cannot be deleted, and whole maps cannot be deleted through the API at all. There is no undo — read the map and check the id first.

GET /api/kb/v1/maps/{id}/rules

Automation rules of the map (“when X → do Y”) — id, name, enabled, trigger, conditions, actions, last_fired (last time-based run — schedule/deadline_approaching; empty for event rules) and last_error (a non-empty error means the rule is broken and the map owner has been notified).

POST /api/kb/v1/maps/{id}/rules

Creates a rule. Requires read_write. Limits are structural only: 50 rules per map, 10 actions and 20 conditions per rule — runs are never counted or billed. A rule applies to future events only, never retroactively.

FieldRequiredMeaning
nameyesrule name (max 120 chars)
triggeryes{"type": …}node_status_changed (optional status), node_unblocked, deadline_approaching (when: before/overdue, days 0–365), node_created, file_uploaded, schedule (freq: daily/weekly, weekday 1–7, hour 0–23)
actionsyesarray of 1–10 actions, executed in order: set_status, set_owner (member e-mail or the dynamic target deputy_of_node_owner), set_deadline (date or relative_days) — these three accept target: trigger_node (default) / parent / a node id, move_node (to = id of the new parent; moves the TRIGGER node and appends it at the end of its new row — the kanban move; the apex, a vanished target or a cycle = an acknowledged skip in the log), create_subnodes (items = same tree as add_nodes, max 50 nodes), notify (to: node_owner/deputy_of_node_owner/map_owner/e-mail, message), run_agent (agent_name from the registry)
conditionsnoAND chain of {field, op, value}field: status/owner/deadline/executor_kind/parent (id of the parent node — "card under a column", eq/ne only); op: eq/ne/empty/not_empty/before/after (deadline only, YYYY-MM-DD)
node_idnoscope the rule to one node; required for schedule rules with node-targeted actions
enablednodefaults to true

Scheduled triggers run within the hour after the configured hour (server local time) — no “exactly at midnight” promise; after an outage they catch up the same day. deadline_approaching with when=overdue fires when the deadline is at least days past — it also catches deadlines that expired before the rule existed — and fires once per deadline (a changed deadline may fire again); when=before fires on the exact day (deadline − days). Rule chaining (an action firing another rule) is allowed up to depth 3, then the run ends with an explicit skipped log entry.

Dynamic targets resolve when the rule runs, not when the rule is saved — staff changes never break a rule. deputy_of_node_owner = the deputy of the trigger node's responsible person: org-structure position deputies take precedence (several positions with different deputies → notify goes to all of them, set_owner is skipped with advice to target a specific position), the personal deputy from Organization settings is the fallback. position:<nodeId> / deputy_of_position:<nodeId> target the holder/deputy of an org-structure position (GET /v1/org-structure lists the ids). An unresolvable target (no deputy, vacant or deleted position) skips the action and the run log says so — the rule is not marked as broken. Targets derived from the trigger node need node_id on a map-level schedule rule; position: targets do not.

POST /api/kb/v1/maps/{id}/rules/{ruleId}

Updates a rule. Passing only {"enabled": true/false} toggles it; otherwise send the full new shape (partial field edits are not merged). Editing clears the rule's error state.

POST /api/kb/v1/maps/{id}/rules/{ruleId}/delete

Deletes a rule. Its run log stays (with a name snapshot).

GET /api/kb/v1/maps/{id}/rule-runs

Run log of the map's rules (newest first, max 100). ?rule= limits to one rule. status: ok / failed / skipped (loop safety stop or per-save cap — detail says which).

GET /api/kb/v1/rule-templates

Instance-wide library of rule templates (rule shape without a map or node scope). To “load” a template, send its trigger/conditions/actions to POST …/rules of the target map — an independent copy is created.

POST /api/kb/v1/rule-templates

Creates (or with id updates — author or admin only) a template: name (unique), trigger, actions, optional conditions. A template must not carry node_id and create_subnodes may only target trigger_node. Requires read_write.

POST /api/kb/v1/rule-templates/{id}/delete

Deletes a template (author or admin only). Rules already loaded from it stay in their maps.

GET /api/kb/v1/org-structure

The organization structure (the org map): positions and functions with node ids, holders and deputies. Response: {exists, map_id, positions: [{node_id, title, position_kind, holder, deputy}]} where position_kind is position (defined by the structure) or function (appointed). Use node_id as the dynamic rule target position:<nodeId> (holder) or deputy_of_position:<nodeId> (deputy of that position) — both resolve when the rule runs, so staff changes never break a rule. An archived org map counts as "no structure" everywhere (exists: false, position targets skip). Read-only: holders and deputies are appointed by an admin in the app (Organization settings) — the API-key contract never reads user roles, so there is no write endpoint here.

GET /api/kb/v1/tasks

Tasks you own, tasks on your maps, and tasks assigned to your e-mail.

QueryMeaning
maponly tasks in this map (404 if it is not yours)
statustodo, in_progress or done; anything else is 400

POST /api/kb/v1/tasks

Creates a task. Requires read_write.

FieldRequiredMeaning
titleyes
mapyesA task always belongs to a project
node_idyesA specific goal in that map. Tasks always belong to a goal; the project apex does not accept tasks.
deadlinenoYYYY-MM-DD
descriptionno
statusnotodo / in_progress / done
assignee_emailnoThe assignee gets a notification

POST /api/kb/v1/tasks/{id}

Updates a task: title, status, deadline, description, assignee_email.

Completing a recurring task automatically creates its next occurrence.

A worked example

bash
KEY="kb_user_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
HOST="http://localhost:8090"

# 1) find a map
curl -s -H "Authorization: Bearer $KEY" "$HOST/api/kb/v1/maps"

# 2) read it — and keep `updated`
MAP=abc123
UPDATED=$(curl -s -H "Authorization: Bearer $KEY" "$HOST/api/kb/v1/maps/$MAP" \
          | python3 -c 'import json,sys; print(json.load(sys.stdin)["updated"])')

# 3) add two goals under the apex
curl -s -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d "{\"base_updated\":\"$UPDATED\",\"items\":[
        {\"title\":\"Draft the copy\",\"deadline\":\"2026-08-15\"},
        {\"title\":\"Shoot the photos\"}]}" \
  "$HOST/api/kb/v1/maps/$MAP/nodes"

Error codes

See Error codes for the full table.

fair-code — self-hosting and internal use are free, reselling as a hosted service is not.