Skip to contentSkip to Content
ReferenceBoard API

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

HeaderWhenMeaning
Authorization: Bearer <token>every board routeThe local or standalone bearer, a signed account session, or a source-bound API token.
HostalwaysThe configured public host, or the loopback host locally; anything else gets 421.
If-Match: "<hash>"card edits except comments, trail notes and attachingThe card’s hash from the last read. Creating a card needs no existing hash.
Content-Type: application/jsonwrites with a bodyBodies 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

RouteBodyReturns
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/opsAn operation requestApplies 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/nameBranch 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 bothThe 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.

RouteBodyReturns
GET /auth/login?return_to=<page>, a page of board.server.originsA redirect to GitHub.
GET /auth/callbackGitHub’s code and the signed stateA 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-clone

A 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…", "…": "…"}}}
StatusCodeWhen
400invalid_inputThe body is not the JSON the route takes, or a value is refused, such as a parent that makes a cycle.
401unauthorizedThe bearer is missing or wrong.
403read_onlyboard.server.read_only is true.
404not_foundNo such card, criterion, artifact entry or route.
409staleThe board moved under three attempts in a row. Retry.
412precondition_failedThe card changed since the If-Match hash; current holds it now.
421misdirectedThe Host header does not name the server’s loopback address.
422board_invalidThe edited card would not load, such as a description that adds a ## Trail heading; nothing was written.
428precondition_requiredThe write needs If-Match.
502backendGit 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:

StatusWhen
400A path index, such as {n}, is not a number from 0.
405The route exists but not with that method.
413The 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.