Kkanbanwall

API reference

Last updated 27 October 2026

Everything an agent or a script can do with a board, over HTTP. The same operations are tools on the MCP server at https://kanbanwall.ai/mcp; both run the same code, so they always give the same answers.

Keys

Every request carries a key in the Authorization header. Keys are made in a board's Settings › Agents & CI, shown once, and stored only as a hash.

Authorization: Bearer kw_board_3fa9…
kw_personal_… Acts as the person who made it, on every board they can open, with their role on each.
kw_board_… Opens one board, as an agent with the key's name. Anything else answers 403. It can read and move cards; it cannot change the board's workflow, columns or people.

A missing, unknown or revoked key answers 401. A board the key cannot open answers 404, and a viewer's key answers 403 to anything that writes.

A board's id is in its address: https://kanbanwall.ai/board/<board-id>. The settings page fills it into every example for you.

Answers

Every call below answers 200 with the same shape:

{ "ok": true, "message": null, "data": … }

When the board says no, ok is false and message is a sentence written to be read and acted on, by a person or a model:

{ "ok": false, "message": "Refused: Review is at 6 of 6, and its limit is strict. Finish something there first.", "data": null }

A refusal is an answer, never an error status, so a pipeline does not fail its build because a card was full or not found.

A card, as every call describes it

{
  "key": "IX-2132",             // what people call it: a tracker key or the card's number
  "id": "card-1063",            // the board's own id
  "title": "sync: retry queue for flaky networks",
  "stage": "Review › Open",     // Group › Stage, or "tray" / "on the wall"
  "lane": { "id": "sc-core", "name": "Core" },  // its lane; null when the column has none
  "release": null,              // the release box it shipped in: { "id", "name" }, or null
  "colour": "yellow",
  "people": ["Leo"],
  "days": 3,                    // days its clock has run; null before it starts
  "goal": 4,                    // the board's cycle-time goal, in days
  "overGoal": false,
  "cycleDays": null,            // days from start to stop, once it stopped
  "finished": false,
  "reopens": 1
}

Naming things

List cards

GET /api/boards/{board}/cards?stage=Review&person=Maya&q=retry&overGoal=true
GET /api/boards/{board}/cards?stage=Backlog&lane=Mobile

The cards on the wall, longest-running first. Every parameter is optional: stage a group or a stage, lane a lane by its exact name or its id (looked for in the columns of stage when one is given), person somebody on the card, q words from the title or the key, overGoal only cards past the goal. data is a list of cards. A lane that is not there, or whose name fits more than one, is refused with the lanes there are, so an empty list always means no cards.

One card, with its history

GET /api/boards/{board}/cards/{key}

A card, plus history: every change, oldest first, with who made it.

"history": [
  { "at": "2026-09-05T07:00:00Z", "what": "to Implement › Open (clock started)", "by": "Leo" },
  { "at": "2026-09-09T14:12:00Z", "what": "to Review › Open", "by": "Leo" },
  { "at": "2026-09-10T16:40:00Z", "what": "to Deploy › Merged (clock stopped)", "by": "GitHub CI" }
]

Workflow

GET /api/boards/{board}/workflow

The stages in order, each with its role (backlog, clock-start, work, parked, clock-stop, archive) and how many cards it holds; every group's WIP limit, its mode (strict refuses, relaxed warns) and its count; the goal; and where the clock starts and stops.

Also columns, the wall's columns left to right, each with the stage it shows, its lanes in order, empty ones included, and its releases (the boxes of the released column, top to bottom, as Releases describes them; empty everywhere else); and colours, the colours a card can be:

"columns": [
  { "id": "c-backlog", "name": "Backlog", "stage": "Backlog › Backlog",
    "lanes": [
      { "id": "sc-core",   "name": "Core",   "order": 1, "cards": 4 },
      { "id": "sc-mobile", "name": "Mobile", "order": 2, "cards": 0 } ],
    "releases": [] },
  { "id": "c-impl-open", "name": "Open", "stage": "Implement › Open", "lanes": [], "releases": [] },
  …
  { "id": "c-releases", "name": "Releases", "stage": "Deploy › Released", "lanes": [],
    "releases": [ { "id": "r-2140", "name": "2.14.0", "published": true, "cards": 3, … } ] }
],
"colours": ["yellow", "blue", "pink", "mint", "lilac", "orange", "red", "grey", "navy", "white",
  "ink"]

Stats

The MCP tool get_stats answers per period (sprint): cards finished, median, 85th percentile and mean cycle time in days, reopens and abandoned work. Over HTTP the same numbers are GET /api/boards/{board}/stats?count=3.

Create a card

POST /api/boards/{board}/cards
Content-Type: application/json

{ "title": "Flaky login test", "stage": "Implement", "people": ["Maya"], "number": "IX-2201", "colour": "pink" }

Only title is needed. colour is one of the palette Workflow lists, yellow when left out. Without a stage the card waits in the tray; with one, it goes to the foot of that stage's column, and a strict WIP limit can refuse it (the card is then written and left in the tray, and the message says so).

Stages and lanes

A stage is a step of the workflow, Review › Open. Moving a card to another stage is a transition: its clock may start or stop, a move backwards counts as a reopen, a WIP limit can refuse it, and it is what the card's history and the stats are made of.

A lane is a subcolumn of a column on the wall, a team splitting its Backlog into Core and Mobile. It is only a place. Moving a card to another lane of the same stage changes where it sits and nothing else: no clock, no reopen, no transition. Lanes are named by the team; their ids, like sc-core, stay the same when they are renamed. A column that is not split has no lanes, and its cards say "lane": null.

Move a card

POST /api/boards/{board}/cards/{key}/move
Content-Type: application/json

{ "stage": "Review" }
{ "lane": "Mobile" }
{ "stage": "Backlog", "lane": "sc-mobile" }

With a stage the card lands at the foot of the first column showing it, exactly as if a hand had put it there: the clock starts or stops where the board says, a move backwards counts as a reopen, and a strict limit refuses it. A card already there changes nothing.

With a lane (its exact name or its id) it lands at the foot of that lane. A lane alone keeps the card in its stage: only where it sits changes. Given with a stage, the lane must be one of that stage's columns. A lane that is not there, fits more than one, or belongs to another stage is refused, naming the lanes there are, and nothing moves. The answer is the card as it is now, its stage and lane included.

A card's colour

POST /api/boards/{board}/cards/{key}/colour
Content-Type: application/json

{ "colour": "pink" }

One of the palette Workflow lists: yellow, blue, pink, mint, lilac, orange, red, grey, navy, white, ink. Anything else is refused with that list. A colour is decoration: it never changes a card's stage or its clock.

Releases

A release is a box in the released column: the cards that shipped together, drawn as a bordered stack with the release's name on it. A card inside is in the released stage like any other, and also says which release it went out in. Releases are the team's and the pipeline's, not the board's shape: any key that can move cards can make one.

GET /api/boards/{board}/releases
"data": [
  { "id": "r-2140", "name": "2.14.0", "column": "Releases", "stage": "Deploy › Released",
    "published": true, "publishedAt": "2026-09-08T16:00:00Z",
    "cards": 3, "keys": ["IX-2131", "IX-2127", "1058"] },
  { "id": "r-2132", "name": "2.13.2", "column": "Releases", "stage": "Deploy › Released",
    "published": false, "publishedAt": null, "cards": 2, "keys": ["1049", "1047"] }
]

Top to bottom as the wall draws them, the newest first.

POST /api/boards/{board}/releases
Content-Type: application/json

{ "name": "2.15.0" }

A new, empty box at the top of the released column. column names which one, by name or id, when the wall has several. A release of that name already there is answered as it is, not made twice. data is the release as above.

POST /api/boards/{board}/cards/{key}/move
Content-Type: application/json

{ "release": "2.15.0" }

Ships a card: it goes to the foot of that box, which is a move into the released stage with everything that means for the clock and the history. A release that is not there is refused with the ones there are; a card already in it changes nothing. A lane cannot go with it (a box has no lanes), and a stage may only be the released one.

A pipeline does the same with a CI event: deployed with a version ships the card into the box of that name, and makes the box when it is the first the board hears of that version.

PATCH /api/boards/{board}/releases/{release}
Content-Type: application/json

{ "name": "2.15.1", "published": true }

Renames a box, marks it published, or not published after all ("published": false); either or both. {release} is its name or its id. A name another box has is refused; what is already so changes nothing. data is the release as it is now.

DELETE /api/boards/{board}/releases/{release}

Takes the box away. Its cards stay in the released column, ungrouped, and each card's history says it left the box.

A client that already speaks commands does the same through the commands endpoint below, by the release's id, and resizes a box there too:

{ "type": "createRelease", "name", "columnId"? } the same as POST …/releases, for a client that already speaks commands
{ "type": "renameRelease", "releaseId", "name" } a name no other release on the board has
{ "type": "publishRelease", "releaseId", "published"? } marks it PUBLISHED on the wall, now; "published": false takes it back
{ "type": "resizeRelease", "releaseId", "minHeight" } the least height the box is drawn with, in wall pixels; null lets the cards decide
{ "type": "removeRelease", "releaseId" } the box goes; the cards in it stay in the column, released still, as its own stack
{ "type": "moveCardToRelease", "cardId", "releaseId" | "name" } what move with a release sends; a name nobody has makes the box

Change, file or delete a card

Everything else a hand does to a card goes through the endpoint the wall itself uses. It takes one command at a time, names the card by its id (the id every call above gives with the card) and needs two more things:

POST /api/boards/{board}/commands
Content-Type: application/json

{
  "commandId": "retitle-card-1063-2026-09-25T10:02",
  "expectedVersion": 812,
  "command": { "type": "retitleCard", "cardId": "card-1063", "title": "sync: retry queue, with backoff" }
}
{ "type": "retitleCard", "cardId", "title", "number"? } Change what the card says. number is the key shown before the title: leave it out to keep it, send null to clear it.
{ "type": "setCardColour", "cardId", "colour" } One of yellow, blue, pink, mint, lilac, orange, red, grey, navy, white, ink.
{ "type": "postMessage", "cardId", "body" } Say something in the card's conversation, as the key.
{ "type": "archiveCard", "cardId" } File it away: off the wall, still counted in the stats for the period it finished in.
{ "type": "deleteCard", "cardId" } Throw it away: off the wall and out of the stats, as if the work had never been written. Nothing is destroyed; the card waits in the board's bin and its history is kept.
{ "type": "restoreCard", "cardId" } Put a filed or deleted card back in the column it was in. Such a card is no longer in List cards; find its id with GET /api/boards/{board}/offwall, which lists every card off the wall.

Filing, deleting and restoring a card that is already that way change nothing.

The answer is the command's own shape, not the data shape above:

{ "ok": true, "reason": null, "message": null, "version": 813, "events": [ … ] }
200, "ok": false The board said no, and message says why: unknown card for an id it does not have.
409, "reason": "version" Somebody changed the same card since expectedVersion. Nothing was applied; the answer carries the current version and what you missed. Look again and resend with that version (the same commandId is fine).
400 A command the board does not know, or one missing commandId.
403 A viewer's key, or a command that changes the board's shape (its workflow, columns or people), which only an owner may send.

Retitling, filing and deleting are not MCP tools yet: an agent on MCP can create cards, move them between stages, lanes and releases, make, rename, publish and remove releases, and change a card's colour, but not edit cards otherwise.

CI events

POST /api/boards/{board}/ci
Content-Type: application/json

{
  "event": "merged",
  "branch": "IX-2132-retry-queue",
  "title": "Retry the sync queue",
  "message": "Merge pull request #412 from acme/IX-2132-retry-queue",
  "repo": "acme/vault",
  "pr": 412,
  "sha": "9f1c2e7",
  "id": "acme/vault-8812"
}
Field Meaning
event merged: the card goes to the stage where the clock stops. deployed: to the released stage. pr-opened: to Review.
version deployed only: the card goes into the release box of that name, "version": "2.15.0", made on the spot the first time the board hears of it. Without it the card goes to the column's own stack.
branch the branch that was merged; the first place the card is looked for
title, message the pull request's title and the merge commit's subject; looked in after the branch
id names the delivery. Send the same id twice and the second changes nothing, so retries are safe.
repo, pr, sha, url kept for the record; they never pick the card

data is the card as it is afterwards. When no card matches, ok is false and the board is left alone. A ready-made GitHub Actions workflow is in Agents and CI.

How a merge finds its card

A merge never says which card it was, so it is read from what the merge says about itself. The candidates are tried in this order, and the first one that is a card on the wall wins:

  1. A tracker key in the branch name, anywhere in its last part: IX-2132-retry-queue, feature/IX-2132, maya/ix-2132-retry → card IX-2132.
  2. A number the branch name starts with: 1063-sync-retry, feature/1063_sync → card 1063.
  3. The branch named in the merge commit, by the same two rules, for when branch is the target branch rather than the merged one: "Merge pull request #412 from acme/1063-sync" → card 1063.
  4. A tracker key in the title or the commit: "IX-2132: retry the queue" → card IX-2132.
  5. An issue the pull request says it closes: "Retry the queue (fixes #88)", "Closes #88", "resolves #88" → card #88, which is how a card imported from GitHub Issues is found. When the board gives the repository's cards a prefix, the event's repo says which: "fixes #88" in a pull request of acme/vault is card vault#88. An issue of another repository can be named the way GitHub writes it: "fixes mobile#88", "fixes acme/mobile#88".

The pull request's own number is never used: pull requests and cards are numbered separately, and PR #1063 has nothing to do with card 1063. A card can only be found if it has a key or a number; the simplest habit is to start every branch with one.

MCP

The MCP server speaks Streamable HTTP at https://kanbanwall.ai/mcp, with the same key in the same header. Its tools are list_boards, get_workflow, list_cards, get_card, list_releases, get_stats, create_card, move_card, set_card_colour, create_release, update_release, remove_release, mark_merged and mark_deployed; each takes a board by id, address or name, which a board key may leave out. Setting it up in Claude Code and other clients is in Agents and CI.