REST API
Everything the interface does goes through this API, and so can you.
Authenticating
Section titled “Authenticating”Every request needs one of:
- Bearer token —
Authorization: Bearer cp_...for a token created in the app, orcpat_...for one issued through the OAuth connector flow. - Session cookie — what the browser itself uses.
POST /api/auth/loginwith a username and password sets an httpOnly__Host-bp_sessioncookie holding an opaquecps_token;POST /api/auth/logoutrevokes 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.
Endpoints
Section titled “Endpoints”| 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 |
Conventions
Section titled “Conventions”- A method a path does not implement answers 405 with an empty body — no
Allowheader, 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.OPTIONSon that path answers 204 with anAllownaming the methods that do exist, which is where to look when a call starts failing this way. The two/.well-knowndocuments are the exception: they answerOPTIONSthemselves, as a CORS preflight, withAccess-Control-Allow-Methodsand noAllow. HEADfollowsGET. It is answered by theGEThandler 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
userisnullon the two the scheduled repository sync writes (pr_linked,pr_unlinked) — a client reading/api/projects/[id]/tasks/[taskId]/activitymust 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/forgotanswers identically whether or not the account exists,POST /api/auth/resetspends a one-hour, single-use token, andPOST /api/auth/confirm-emailspends 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/mewith a newemail, on an instance that can send mail, does not store it: the answer carriespendingEmailalongside 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.GETandDELETE /api/users/me/email-changeread and cancel the pending change; like the address itself, they refuse a machine credential.POST /api/projectsanswers 409 That key is already used by another board when the key is taken, rather than failing the request.GET /api/usersis for instance administrators and lists people only.?include=machinesadds the machine accounts — thepmidentity and each enrolled worker’sworker-…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
categorybounded by the 50 a category name may be — on bothPOSTandPUT, 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/instanceis 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/userscounts the accounts itself and refuses a second bootstrap whatever a client believes. On an instance with no accounts that request also needssetupCode— 403 without the right one, 429 once a source has sent too many wrong ones, which with a configuredBOOTSTRAP_TOKENholds even for the right one — and a username ofpmorworker-plus 24 hex characters is refused with 400 on any instance.POST /api/projects/[id]/pm/reviewruns 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/settingsrefuses 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-resourceand/.well-known/oauth-authorization-server, with registration at/oauth/registerand the flow at/oauth/authorizeand/oauth/token.