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.
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.
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.
{
"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
}
IX-2132, 1063, #88, or
vault#88 when the board gives a GitHub repository's cards a prefix (it has to
once it follows two repositories). Case and a leading # do not matter; a
prefixed number is only ever itself. In a path, write # as %23.
Review › Open,
Review - Open, or just the group, Review, which means its first
stage when moving and all of its stages when listing. A name that matches nothing is
answered with the list of stages there are.
2.15.0, or by the id
Releases gives.
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.
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" }
]
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"]
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.
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).
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.
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.
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.
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 |
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:
commandId: any string of yours, unique per change. Sending the same one again
answers what the first answered and changes nothing, so a retry is always safe.
expectedVersion: the board's version you last saw. Ask for it cheaply with
GET /api/boards/{board}/events?sinceSeq=999999999, which answers
{ "version": 812, "events": [] }. It does not have to be the very latest: a
command about one card still applies on a board that has moved on, as long as nobody else
changed that same card in between.
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.
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.
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:
IX-2132-retry-queue, feature/IX-2132,
maya/ix-2132-retry → card IX-2132.
1063-sync-retry,
feature/1063_sync → card 1063.
branch is the target branch rather than the merged one: "Merge pull request
#412 from acme/1063-sync" → card 1063.
IX-2132.
#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.
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.