Skip to content
commission

HTTP API

You will be able to drive commission over HTTP — every route, its request and response shape, how authentication works, and which status code each kind of failure produces — from a client that is not a shell.

The API is a thin wrapper over the same core functions the CLI calls. There is no second implementation, so a route cannot behave differently from its command.

Running it

commission serve --port 4400
commission serving http://localhost:4400  (db: /Users/you/.commission/commission.db)
  humans:  http://localhost:4400/
  agents:  http://localhost:4400/v1/...  (no token set — local mode)

Two surfaces on one port: / is the read-only web view for people, /v1 is the API. Add --sync 5m to run the background reconciliation loop; it is pull-only, and outbound plans are logged rather than pushed — see interop.

Authentication

Set COMMISSION_TOKEN in the server’s environment and mutations require it:

COMMISSION_TOKEN=s3cret commission serve --port 4400
no COMMISSION_TOKENCOMMISSION_TOKEN set
GETallowedallowed
POSTallowedrequires Authorization: Bearer <token>

A missing or wrong token on a mutation returns 401:

{ "error": "unauthorized", "message": "mutations require Authorization: Bearer <COMMISSION_TOKEN>" }

Reads are never gated. That is deliberate: it is what lets you expose the board to a team read-only while writes stay agent-only. If you need reads protected too, put it behind something that does authentication properly — commission’s token is a single shared secret, not an identity system.

The token is not an actor. POST /v1/tasks/:id/approve resolves the actor from the server process (COMMISSION_ACTOR), never from the request, and refuses anyone who is not a human. A caller cannot name itself into a person, so the endpoint grants no power the CLI does not already grant to whoever runs commission.

Version handshake

GET /v1/version
{ "commission": "0.1.0", "schema": 11, "config": 1 }

Three numbers a client can check before it talks: the binary’s semver, the database’s schema version, and the .commission.json format this binary speaks. What each one promises is in compatibility.md.

Routes

Reads

methodpathreturns
GET/v1/version{ commission, schema, config }
GET/v1/projectsevery project row
GET/v1/projects/:slug/boardthe board
GET/v1/projects/:slug/readythe ready frontier
GET/v1/projects/:slug/inboxthe inbox?actor=, ?all=true, ?limit=
GET/v1/next?project=<slug>preview: the selection plus its bundlenever claims
GET/v1/tasks/:idthe context bundle
GET/v1/primethe agent operating manual, as text/plain?project=
GET/v1/search?q=<query>full-text matches — ?project= scopes it
GET/v1/eventsthe event feed — ?project=, ?task=, ?since=, ?since_id=, ?limit=

GET /v1/events returns an array with payload already parsed. Page with since_id, not since: ids are monotonic, timestamps are a race.

Writes

methodpathbodyreturns
POST/v1/next{ project, leaseMs? }atomically selected and claimed task + bundle
POST/v1/tasksa task definition201 with the created task
POST/v1/tasks/:id/transition{ to, force?, expectedVersion? }{ task, from }
POST/v1/tasks/:id/approve{ kind?, note?, role? }201 with the approval record
POST/v1/tasks/:id/claimthe claim row
POST/v1/tasks/:id/releasethe actor that held it, or null
POST/v1/tasks/:id/notes{ text }201 { ok: true }
POST/v1/tasks/:id/evidence{ items: [{ kind, value, note? }] }201 { ok: true }
POST/v1/applyan apply document201 with what was created and updated

The atomic select-and-claim contract

GET /v1/next and POST /v1/next look similar and are not:

  • GET previews. It selects a task, builds its bundle, and changes nothing. Two clients calling it get the same answer.
  • POST selects and claims, in one operation. The claim is decided by a single conditional statement in SQLite, so two clients calling it concurrently receive different tasks. There is no window in which both can win, and no advisory lock to hold.

This is the orchestration primitive. commission next --claim and the commission_next MCP tool are the same call; a fleet of workers needs no coordinator beyond it.

curl -s -X POST localhost:4400/v1/next \
  -H 'content-type: application/json' \
  -d '{"project":"myapp"}'

The response is { task, claimed, skipped, selectedBy, routing, selectionReason, bundle }. task is null when the frontier is empty. skipped says which tasks were passed over and why — usually already claimed by someone else. selectedBy names the project’s selection policy and routing explains who may hold it and what would change that. leaseMs: 0 claims without expiry; omit it for the default eight hours.

Status codes

codewhen
200ok
201created — tasks, notes, evidence, approve, apply
401mutation without a valid bearer token, when COMMISSION_TOKEN is set
404the project, task, or record does not exist
409optimistic-concurrency conflict — expectedVersion no longer matches
422refusal: understood and declined
500unexpected error

Bodies for 404, 409, and 422 are commission’s refusal shape — error, message, missing, hint — documented once in exit codes and the refusal shape. A 422 is an instruction:

{
  "error": "refusal",
  "message": "cannot complete myapp-1: 1 unchecked criteria",
  "missing": ["criterion 0: tested"],
  "hint": "tick each with 'commission check myapp-1 0' — only once it is actually true; or --force to override (logged)"
}

A missing required query parameter is also a 422GET /v1/next without ?project= and GET /v1/search without ?q= both refuse rather than guessing.

What the API deliberately does not cover

Project administration, configuration, import, and the workspace commands are CLI-only. They touch the local filesystem, the git checkout, and vendor CLIs the server has no business reaching on a caller’s behalf. If you need them remotely, run commission where those things live.


Part of Reference.

This page is docs/reference/http.md in the Commission repository, rendered in place — the site keeps no copy of it. The repository is private, so there is no edit link to follow.