Skip to content
Board Planner
GitHubOpen the app

REST API

Everything the interface does goes through this API, and so can you.

Every request needs one of:

  • Bearer tokenAuthorization: Bearer cp_... for a token created in the app, or cpat_... for one issued through the OAuth connector flow.
  • Session cookie — what the browser itself uses. POST /api/auth/login with a username and password sets an httpOnly __Host-bp_session cookie holding an opaque cps_ token; POST /api/auth/logout revokes it. The password is never a credential the API accepts on an ordinary request.

There is no Basic auth. Sending a username and password on a request header authenticates nothing — a script or CI job wants a bearer token instead. A presented bearer token that resolves to nothing is refused rather than quietly falling back to the cookie.

A token never exceeds its owner’s access, and a scoped token is further limited to its chosen projects. The check happens centrally, so REST and MCP behave identically.

Group Endpoint Methods
Auth /api/auth/me, /api/auth/login, /api/auth/logout, /api/auth/forgot, /api/auth/reset, /api/auth/confirm-email, /api/auth/instance GET, POST
Users /api/users, /api/users/me, …/me/password, …/me/email-change GET, POST, PUT, DELETE
One user /api/users/[id] PUT, DELETE
Tokens /api/tokens GET, POST, DELETE
Projects /api/projects, /api/projects/[id] GET, POST, PUT, DELETE
Members /api/projects/[id]/members GET, PUT, DELETE
Member lookups /api/projects/[id]/members/candidates, /api/projects/[id]/assignable-users GET
Columns /api/projects/[id]/columns GET, PUT
Categories /api/projects/[id]/categories POST, PATCH, DELETE — read them off GET /api/projects/[id], which carries the whole list
Custom fields /api/projects/[id]/custom-fields, …/[fieldId] GET, POST, PATCH, DELETE
Templates /api/projects/[id]/templates POST, PUT, DELETE — both writers apply the same rules to a name they are sent — a name another template holds is 409, a blank or non-string one 400 — and a template may still keep, recase or trim its own. PUT may also omit name entirely, which changes nothing; POST requires it
Tasks /api/projects/[id]/tasks, …/tasks/[taskId], …/tasks/reorder, …/tasks/suggest GET, POST, PUT, DELETE
Task actions …/tasks/[taskId]/status, /watch, /links, /activity, /gitlab-activity GET, POST, PATCH, DELETE
Worker hand-over …/tasks/claim, …/tasks/[taskId]/release POST
Hand-over readiness /api/projects/[id]/handover GET — readable by anyone with access to the board, including through an API token or an MCP connection. It carries what the task screen needs to say why an agent will not run: whether the reader can change the board (canAdmin), its repository, whether runs are on or locked by the instance, the column roles, and machine — the best state among the reader’s own machines that carry this repository, from worst to best stale, failing, stopped, paused, live. none means no machine of theirs carries it. It names no other person and no other machine
Refused changes …/tasks/[taskId]/decision GET, POST — the POST refuses a Bearer token outright, whatever its access: answering needs a browser session
Agent runs /api/projects/[id]/runs GET, POST
Project agent /api/projects/[id]/agent PUT
Comments …/tasks/[taskId]/comments, …/comments/[commentId] GET, POST, PUT, PATCH, DELETE — PATCH toggles a reaction
Sprints /api/projects/[id]/sprints, …/sprints/[sprintId] GET, POST, PUT, DELETE
Stats /api/projects/[id]/stats GET
Audit /api/projects/[id]/audit GET
Webhooks /api/projects/[id]/webhooks POST, PUT, DELETE — read them off GET /api/projects/[id], with each URL masked
Channels /api/projects/[id]/notifications GET, POST, PUT, DELETE — GET lists them with each URL masked
Git /api/projects/[id]/github/sync, …/gitlab/sync, …/coda/sync POST
AI /api/projects/[id]/ai/generate-task GET, POST — GET says whether generation is enabled
PM agent /api/projects/[id]/pm/chat, …/pm/messages, …/pm/interrupt, …/pm/usage, …/pm/review, …/pm/mcp-test, …/pm/mcp-oauth/start, …/pm/mcp-oauth/disconnect GET, POST
OAuth /api/oauth/clients, /api/oauth/connections, /api/pm/oauth/callback GET, DELETE
Notifications /api/notifications, …/read, …/unread-count GET, PATCH
Notification preferences /api/users/me/notifications, …/notifications/[projectId] GET, PUT, DELETE
Search /api/search GET
My tasks /api/tasks/mine GET
Uploads /api/uploads, /api/uploads/[fileId] GET, POST
Workers /api/workers/register, /api/workers/[id], …/heartbeat, …/command, …/events, …/stream, …/[id]/projects, …/decisions GET, POST, PUT, PATCH
Worker enrolment /api/workers/enrolment, …/enrolment/device, …/device/[userCode], …/[userCode]/approve, …/device/token GET, POST
Projects (ordering) /api/projects/reorder PUT
Admin /api/admin/agents, /api/admin/agents/[projectId], /api/admin/workers, /api/admin/audit, /api/admin/email, /api/admin/runs, /api/settings GET, POST, PUT, PATCH
Entitlements /api/entitlements GET — any signed-in caller, with any credential. The instance’s plan as stored, { plan, features, expiresAt }: plan is free or pro, features lists feature keys granted individually and may be empty on pro, which grants every one, and expiresAt is null unless the plan carries an end date. It does not apply that date: plan still reads pro after the end date and its grace period have passed, while the features themselves are refused. A fresh instance answers { "plan": "free", "features": [], "expiresAt": null }
Agents /api/agents, /api/agents/[agentId] GET, POST, PUT, DELETE
Agent blocks /api/agent-blocks, /api/agent-blocks/[blockId] GET, POST, PUT, DELETE
MCP /api/mcp POST
  • A method a path does not implement answers 405 with an empty body — no Allow header, and none of the { "error": … } shape the rest of this API uses, so a client parsing the failure as JSON gets an exception rather than a reason. OPTIONS on that path answers 204 with an Allow naming the methods that do exist, which is where to look when a call starts failing this way. The two /.well-known documents are the exception: they answer OPTIONS themselves, as a CORS preflight, with Access-Control-Allow-Methods and no Allow.
  • HEAD follows GET. It is answered by the GET handler where there is one, and 405s where there is not. No route overrides it.
  • Task keys are KEY-NUMBER (ORB-14). Numbers are per project and allocated atomically, so concurrent creates never collide.
  • Custom field values are keyed by field id. Ask the project endpoint which fields exist rather than assuming names.
  • Activity logging is fire-and-forget: it never blocks or fails a request. A row’s user is null on the two the scheduled repository sync writes (pr_linked, pr_unlinked) — a client reading /api/projects/[id]/tasks/[taskId]/activity must expect it, and the board renders such a row as The repository sync.
  • Password reset and address confirmation are the endpoints that take a link’s token instead of a credential. POST /api/auth/forgot answers identically whether or not the account exists, POST /api/auth/reset spends a one-hour, single-use token, and POST /api/auth/confirm-email spends the 24-hour token sent to a new address — 400 for a link unknown, expired or used, 409 when another account has taken the address meanwhile, which leaves the link usable. All three are throttled per source and refuse a request whose origin is not in the instance’s allowlist.
  • PUT /api/users/me with a new email, on an instance that can send mail, does not store it: the answer carries pendingEmail alongside the unchanged account, and the address moves once its link is confirmed. 429 past three confirmation emails in 15 minutes, 503 when the instance does not know its own address. GET and DELETE /api/users/me/email-change read and cancel the pending change; like the address itself, they refuse a machine credential.
  • POST /api/projects answers 409 That key is already used by another board when the key is taken, rather than failing the request.
  • GET /api/users is for instance administrators and lists people only. ?include=machines adds the machine accounts — the pm identity and each enrolled worker’s worker-… account — which Settings → Users never shows. 403 for a signed-in account that is not an administrator.
  • Limits answer 400 past them: 200 acceptance criteria, 100,000 characters of description, 20,000 of comment, 20 webhooks and 20 chat channels per project, 2,048 characters of webhook or channel URL, 100 of channel name, 50 categories and 50 task templates per project. A task template is held to what it becomes: 100 characters of template name, 200 of title, 100,000 characters each of description and acceptance-criteria text, and a category bounded by the 50 a category name may be — on both POST and PUT, either of which also refuses any of those fields sent as something other than a string. AI generation takes a prompt of up to 10,000 characters, answers 429 past 20 per person in 15 minutes or past the project’s cap for 24 hours, and 409 while the same person already has one running.
  • GET /api/auth/instance is the one endpoint that takes no credential at all. It answers { "unclaimed": true } on an instance with no accounts yet, so the login page knows whether to offer Create Account. It is not the gate: POST /api/users counts the accounts itself and refuses a second bootstrap whatever a client believes. On an instance with no accounts that request also needs setupCode403 without the right one, 429 once a source has sent too many wrong ones, which with a configured BOOTSTRAP_TOKEN holds even for the right one — and a username of pm or worker- plus 24 hex characters is refused with 400 on any instance.
  • POST /api/projects/[id]/pm/review runs a board review now, for a project owner or an instance administrator in a browser session: 202 once it has started, 403 for any API token or connected app, 503 when the instance has no model credentials, and 409 naming why when the agent is off or locked for the project, the day’s turn or token cap is reached, or another PM turn is running.
  • PUT /api/settings refuses a machine credential with 403, whatever its role: the instance AI defaults take an interactive session, and each change is written to the audit log.
  • OAuth discovery lives at /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server, with registration at /oauth/register and the flow at /oauth/authorize and /oauth/token.