Board API
folio-board-server api serves one versioned HTTP API, locally on loopback or
behind HTTPS. The live kanban calls it; so can a script.
Local Git, hosted GitHub and
SQLite guides cover the setup.
curl -s http://127.0.0.1:4350/v1/health \
-H "Authorization: Bearer $TOKEN"{"store":"local","location":"/home/ana/widgets/board","read_only":false,"actor":"ana","rev":"6ade54d…"}A hosted server also names the board’s source, its repository and branch,
and actor is the signed-in login: "source":"acme/roadmap@board".
A SQLite server reports "store":"sqlite", its database location and a
source such as "sqlite:board". It uses the same routes and file hashes;
revision identifiers address database snapshots rather than Git commits.
Every request carries the bearer
| Header | When | Meaning |
|---|---|---|
Authorization: Bearer <token> | every board route | The local or standalone bearer, a signed account session, or a source-bound API token. |
Host | always | The configured public host, or the loopback host locally; anything else gets 421. |
If-Match: "<hash>" | card edits except comments, trail notes and attaching | The card’s hash from the last read. Creating a card needs no existing hash. |
Content-Type: application/json | writes with a body | Bodies are at most 256 KB. |
A card’s hash is the git blob SHA-1 of its file. GET /v1/cards/{id} returns
it as hash and as the ETag header; every write returns the new one.
Routes read the board and edit one card at a time
| Route | Body | Returns |
|---|---|---|
GET /healthz | — | Public readiness: {"status":"ready"}. No bearer required. |
GET /v1/tokens | — | This signed-in account’s token metadata; never their secrets. Account mode only. |
POST /v1/tokens | {"name":"Terminal","source":{"kind":"sqlite","id":"roadmap","params":{}},"permission":"read","expires_in_days":30} | A one-time token and its key metadata. Normal account session required. |
DELETE /v1/tokens/{id} | — | Revokes this account’s token; 204. API tokens cannot manage tokens. |
POST /v1/ops | An operation request | Applies up to 32 operations to one card. Supports if_match and dry_run. |
GET /v1/sources | — | Authorized source descriptors, provider fields, install_url and provider-specific warnings. |
POST /v1/sources/options | {"kind":"github","id":"owner/repo","params":{"branch":"board","path":""}} | Provider-defined configuration fields and options. Database sources need no branch or folder. |
POST /v1/sources/open | {"kind":"sqlite","id":"roadmap","params":{}} | A token bound to the selected source; the same route accepts GitHub references. |
GET /v1/repositories | — | The signed-in person’s authorized repositories and install_url. Account mode only. |
GET /v1/repositories/branches | ?repo=owner/name | Branch names for an authorized repository. Account mode only. |
POST /v1/repositories/open | {"repo":"owner/name","branch":"board","path":""} | A new token bound to this board. The directory must contain a valid board. |
POST /auth/logout | — | Invalidates the account’s credentials and every board token from that sign-in. |
GET /v1/health | — | The store, its location, read_only, the actor and the head revision. |
GET /v1/board | — | rev, title, columns with their card ids, icons, every card, and warnings. |
POST /v1/cards | {"id": "first-idea", "markdown": "---\ntitle: First idea\nstatus: ideas\n---\n"} | Creates a validated card through the selected store and returns the outcome with status 201. Existing ids are refused. |
GET /v1/cards/{id} | — | One card: hash, fields, description, criteria, comments, trail, artifacts. |
POST /v1/cards/{id}/move | {"status": "done", "after": "other-card"} | The outcome. after is optional; the anchor must be in the destination column and carry an order: rank, as for move --after. |
PATCH /v1/cards/{id}/fields | {"set": {"priority": "high", "size": null}} | The outcome. null removes the field. |
PUT /v1/cards/{id}/description | {"markdown": "…"} | The outcome. |
POST /v1/cards/{id}/criteria | {"text": "…"} | The outcome. Appends one open criterion. |
PUT /v1/cards/{id}/criteria/{n} | {"done": true}, {"text": "…"} or both | The outcome. n counts from 0. Both in one request commit as board: update <id>. |
POST /v1/cards/{id}/comments | {"text": "…"} | The outcome. |
POST /v1/cards/{id}/trail | {"note": "…", "ref": "abc1234"} | The outcome. ref is optional. |
POST /v1/cards/{id}/artifacts | {"kind": "pr", "target": "42", "label": "…"} | The outcome. kind is pr, url, doc or file. A doc or file must already be a file of the board. |
DELETE /v1/cards/{id}/artifacts/{n} | — | The outcome. Removes the n-th artifacts: entry, from 0. |
fields accepts the fields update takes, plus tags,
blocked_by and link; tags, blocked_by and assignee take a list.
GET /editor/v1.js returns the description editor’s script and needs no
bearer.
A doc or file target is a path from the card’s directory, such as
report.md, or from the board as board/cards/<id>/report.md. A file
elsewhere in the project, such as docs/research/analysis.md, gets
400 invalid_input. For a Git board, attach it with
folio-agents board attach. SQLite file artifacts
refer to files imported with the seed; the API does not upload new file contents.
A hosted GitHub server adds three sign-in routes
With a GitHub store and board.server.hosted set, the server answers its public
host, signs people in with GitHub and takes a session instead of the printed
bearer. SQLite uses the same login when board.auth.provider: github is selected.
Explicit provider: token uses the configured workspace bearer instead.
| Route | Body | Returns |
|---|---|---|
GET /auth/login | ?return_to=<page>, a page of board.server.origins | A redirect to GitHub. |
GET /auth/callback | GitHub’s code and the signed state | A redirect to <page>#board-code=<code>, a one-time code valid 60 seconds. |
POST /auth/exchange | {"code": "..."} | token, login, write and expires_at. The token is the bearer for 8 hours. |
In account mode, sign-in does not require access to the deployment repository.
The person lists connected repositories, chooses a branch and directory, and
exchanges that selection for a board-bound session. A board request without a
selection answers 400 invalid_input. A repository outside both the person’s
access and the App installation answers 403 read_only. GitHub permissions
are checked again on each read and write. Readers cannot edit. Each commit is
authored by the signed-in person and committed by the server.
With FOLIO_BOARD_MULTI_REPO=false, the server serves the single repository configured
in board.store; only its collaborators can read it.
A hosted server clones the board’s branch at start into --workdir, replacing what was there, and reads its secrets from the environment: FOLIO_BOARD_SESSION_KEY (at least 32 bytes), FOLIO_BOARD_GITHUB_CLIENT_SECRET, and its own credential for the repository. That credential is a GitHub App installed on the repository, whose private key is in FOLIO_BOARD_GITHUB_APP_KEY or in the file FOLIO_BOARD_GITHUB_APP_KEY_FILE names: the server signs in as the App and renews its one-hour installation token on its own. Without an App key, it takes a token from the variable board.store.token_env names. People sign in with the same App, whose client id is board.server.hosted.github_client_id.
folio-board-server api --project-dir . --workdir /tmp/folio-board-cloneA write returns its revision
Card creation records board: add <id>, on either store.
Git stores create a commit. SQLite creates a database revision; the commit
response field carries its identifier and message, with url: null.
{
"rev": "f607a94…",
"changed": true,
"commit": {"sha": "f607a94…", "message": "board: ship-the-alpha done -> ideas", "url": null},
"card": {"id": "ship-the-alpha", "hash": "b98e2fd…", "status": "ideas", "…": "…"},
"diff": "",
"warnings": []
}A write whose result equals the card’s current bytes commits nothing and
returns "changed": false. The server writes the commit message; a request
cannot set one. The messages are those of the CLI, plus
board: describe <id>, board: check criterion on <id>,
board: update criterion on <id>, board: add criterion on <id> and
board: detach <kind> from <id>. A request that makes two changes, such as a
criterion’s done and text, commits as board: update <id>.
One error envelope, ten codes
{"error": {"code": "precondition_failed", "message": "'ship-the-alpha' changed since you read it",
"hint": "review the current card and retry with its hash", "retryable": false,
"current": {"id": "ship-the-alpha", "hash": "b98e2fd…", "…": "…"}}}| Status | Code | When |
|---|---|---|
| 400 | invalid_input | The body is not the JSON the route takes, or a value is refused, such as a parent that makes a cycle. |
| 401 | unauthorized | The bearer is missing or wrong. |
| 403 | read_only | board.server.read_only is true. |
| 404 | not_found | No such card, criterion, artifact entry or route. |
| 409 | stale | The board moved under three attempts in a row. Retry. |
| 412 | precondition_failed | The card changed since the If-Match hash; current holds it now. |
| 421 | misdirected | The Host header does not name the server’s loopback address. |
| 422 | board_invalid | The edited card would not load, such as a description that adds a ## Trail heading; nothing was written. |
| 428 | precondition_required | The write needs If-Match. |
| 502 | backend | Git or the file system failed. Retry. |
retryable is true for stale, and for a backend failure of git or the
file system on /v1/board, a card read or a write, where the same request can
succeed. Two backend answers carry false: a store failure on /v1/health,
and a request whose worker task failed inside the server.
Three answers bypass the envelope and carry a plain-text or empty body:
| Status | When |
|---|---|
| 400 | A path index, such as {n}, is not a number from 0. |
| 405 | The route exists but not with that method. |
| 413 | The body is over 256 KB. |
No MCP server ships in this release
Not available in this release. Scripts and agents use this API for Git or
SQLite. folio-agents board and the packaged skills operate Markdown/Git
directories; they do not edit SQLite. Tool support
compares the available interfaces.
Operation requests
POST /v1/ops and MCP’s card_apply call the same domain service. Set card
to the id, ops to the operation list, if_match to the hash returned by
GET /v1/cards/{id}, and dry_run to preview without committing.
{"card":"improve-search","ops":[{"op":"move","status":"in-progress"}],"if_match":"<current hash>","dry_run":false}The service accepts move, update, set_description, toggle_criterion,
set_criterion_text, add_criterion, comment, trail, attach and detach operations.
Create a card with POST /v1/cards or MCP’s card_create instead of an op:create.
Every write retains normal read-only and stale-hash checks. Neither actors nor
commit messages are request fields. See API tokens
and MCP.