The API
Everything the Consensus-AI page does goes through the API, and so does everything an LLM does. It is plain HTTP with JSON bodies. On a Local server every request is the owner; on a Public server a request is a Clerk session or a token, and a bare one is refused (plan User Management, Step 4; plan Clerk, Step 5). A request says who it is with the header X-Consensus-Service: <name>, and what it posts is then recorded as the owner through that service, shown as the byline <name> by <owner>; a run the server started carries Authorization: Bearer <run token>, a token the server made for it and forgets when it ends; a token minted on an account's page (plan User Management, Step 3) makes the request that account, allowed in each project what its role folder there allows (Owners, Members, Service, Watchers), refused with 403 and the reason otherwise; a Clerk session token in the same header is its user, written to the registry from the token's claims at the first sight (Step 4), and refused with license when the person's roles need a plan their license lacks (plan Clerk, Step 5: Host for the Administrator, User for an Owner or a Member; the answer carries Plan, the plan wanted, and Has); any other token is refused. Errors are { "Error": "..." } with the status: 400 a bad body, 401 an unknown token (sign in for a missing or stale session, license for a plan the person lacks), 403 not allowed, 404 not found, 409 not allowed in this state or a stale revision.
Four calls
What waits on the caller, across every project:
curl -s -H "X-Consensus-Service: <name>" http://127.0.0.1:3500/api/waiting
A reply on a thread:
curl -s -X POST -H "X-Consensus-Service: <name>" -H "Content-Type: application/json" \
-d '{ "Text": "Agreed; the wording I will apply: ..." }' \
http://127.0.0.1:3500/api/plans/<plan id>/threads/<thread id>/replies
Applying a resolved thread, with the new text of the whole plan and the revision it was made from:
curl -s -X POST -H "X-Consensus-Service: <name>" -H "Content-Type: application/json" \
-d '{ "Outcome": "The section now says ...", "Revision": 4, "Text": "# The plan\n\n..." }' \
http://127.0.0.1:3500/api/plans/<plan id>/threads/<thread id>/apply
A new thread on a passage of the text:
curl -s -X POST -H "X-Consensus-Service: <name>" -H "Content-Type: application/json" \
-d '{ "Text": "Is this the right default?", "Anchor": { "Text": "the words to anchor to" } }' \
http://127.0.0.1:3500/api/plans/<plan id>/threads
An anchor's Text is matched against the text as it reads, without markdown marks: no backticks or asterisks.
The operations
Every route is an operation of one interface definition (src/Operations.js), of which the REST API, the MCP server and a local model's tools are three faces: the name an MCP client calls the tool by, the route the API serves it at, what it does, and its arguments. The path's parameters are arguments too (:id is Id, :pid Project, :fid Folder, :tid Thread, :n Revision, :name Name); a GET takes the rest as its query, anything else as its JSON body; an answer is JSON at 200 unless the operation says 201. The list is rendered from the definition when the docs are built, so it is what the server serves.
meGET /api/me— Who you are to this server: your account, your roles by project, the speakers the rules run over, the default states and the server's version and name.instructionsGET /api/instructions— The instructions for an LLM working with this server: what Consensus is, how to call it, what your user's words mean ("check consensus", "your turn", "build it"), the turn and the build loop; This server first, with the API's address and how to say who you are. Read it first.read_settingsGET /api/settings— The server's settings file, whole, and its path; the Administrator's.write_settingsPUT /api/settings— Writes the server's settings (Name, Port, Scope, States, Owner, Tasks, Llms; the Administrator's): the fields given replace the file's, any other field stays. Answers whether a restart is needed (Scope and Port take effect at the next start). Arguments:Settings(object): the settings to write; the fields may also be given at the top level.list_plansGET /api/plans— Every plan and document you may see, across projects, each with its state and the tally of its threads; State keeps the plans in that state. Arguments:State(string): only the plans in this state.create_planPOST /api/plans(answers 201) — A new plan, or a document (Kind document), in a project: at Parent (a folder, or a plan for a Subplan), else at the project's root (a document: in its Context folder). A plan starts in the project's first state unless State says otherwise. Arguments:Title(string, required);Text(string): the markdown; empty when not given;Project(string): the project's id; the first project when not given;Parent(string): a folder's id, or a plan's id for a Subplan;Kind(string): plan (the default) or document;State(string): one of the project's states; an Owner's to give.read_planGET /api/plans/:id— A plan or document whole: its text at its current revision, its summary with the tally of its threads, its threads with their status, anchor and replies, its project, and whether it is the project's Readme (Context). Arguments:Id(string, required): the plan's or document's id.reportGET /api/plans/:id/report— The plan, its Subplans and all their threads as one HTML page made for printing: the passages marked and numbered, every reply with who and when. Arguments:Id(string, required): the plan's id.report_markdownGET /api/plans/:id/report.md— The same report as markdown. Arguments:Id(string, required): the plan's id.rename_planPUT /api/plans/:id— A plan's or document's new Title; never the Readme's. Arguments:Id(string, required);Title(string, required).write_textPUT /api/plans/:id/text— The plan's or document's whole new text as a new revision, made from Revision (refused with the current revision when the text changed since); the threads stay where they were, an anchor no longer found marking its thread detached. Arguments:Id(string, required);Text(string, required): the whole markdown;Revision(integer, required): the revision the text was made from.delete_planDELETE /api/plans/:id— A plan or document to the trash, its Subplans with it; never the Readme. Arguments:Id(string, required).list_trashGET /api/trash— The trashed plans and documents of every project.empty_trashDELETE /api/trash— Removes every trashed item of every project for good; the owner's.set_statePUT /api/plans/:id/state— Sets a plan's state to one of its project's (Draft, Plan, Working, Finished by default); an Owner's. Arguments:Id(string, required);State(string, required).list_projectsGET /api/projects— Every project you may see with its tree: the folders, the plans with their states and the tally of their threads, the documents of its Context folder, the archive; the id of its Readme (Context) and of its Context folder.create_projectPOST /api/projects(answers 201) — A new project named Name, its folder at Path (/projects/ / when not given); a Path that holds a project already is opened instead, under its own name. Arguments: Name(string): required unless Path holds a project;Path(string): the project's folder.update_projectPUT /api/projects/:pid— A project's settings: its Name, its States, the Include, Exclude and Commands lists a run works within, the Tasks it enables, the Work options GitSync and AutoCommit, and the Llm connection its buttons run through; a state still used by a plan cannot be removed. Arguments:Project(string, required): the project's id;Name(string);States(array): the state names, or lines of text;Include(array);Exclude(array);Commands(array);Tasks(array): the enabled task ids; null enables them all;GitSync(boolean);AutoCommit(boolean);Llm(string): a connection's id, or null;Version(integer): the project version the change was made from; refused when it moved on.delete_projectDELETE /api/projects/:pid— Deletes an empty project (its Readme goes to the trash with it); never the last project, and never one that still holds items. Arguments:Project(string, required): the project's id.move_projectPOST /api/projects/:pid/move— Moves a project in the display order: just before the project Before, or to the end without it. Answers every project as list_projects does. Arguments:Project(string, required): the project's id;Before(string): the id of the project it goes just before.export_projectGET /api/projects/:pid/export— A whole project as one JSON object: its tree, every plan and document with its threads and every revision (the export format); the owner's. Arguments:Project(string, required): the project's id.export_itemGET /api/items/:id/export— A folder, a plan or a document as one JSON object of the export format, with everything under it; never the Readme or the Context folder on their own; the owner's. Arguments:Id(string, required): the item's id.import_projectPOST /api/projects/import(answers 201) — Imports an export file: as a project (one that is here already needs Mode copy or merge), or with Into its items into a project's root or folder with every id new; Preview writes nothing and says what would happen; the owner's. Arguments:Export(object, required): the export file's content;Mode(string): copy or merge, for a project that is here already;Preview(boolean);Into(object): { Project, Parent? }: the project and folder the items go into.create_folderPOST /api/projects/:pid/folders(answers 201) — A new folder in a project, at Parent (a folder) or at the root; never in the Context folder. Arguments:Project(string, required): the project's id;Name(string, required);Parent(string): a folder's id; the root when not given;Version(integer): the project version the change was made from.rename_folderPUT /api/projects/:pid/folders/:fid— A folder's new Name; never the Context folder's. Arguments:Project(string, required): the project's id;Folder(string, required): the folder's id;Name(string, required);Version(integer): the project version the change was made from.delete_folderDELETE /api/projects/:pid/folders/:fid— Deletes an empty folder; never the Context folder, and never one that still holds items. Arguments:Project(string, required): the project's id;Folder(string, required): the folder's id.move_itemPOST /api/items/:id/move— Moves a folder, plan or document into a project's root or one of its folders (a plan into a plan as a Subplan), at the end or just before a child of it, within its project or into another; never the Readme or the Context folder. Arguments:Id(string, required): the item's id;Project(string, required): the project it goes into;Parent(string): a folder's id (or a plan's, for a Subplan); the root when not given;Before(string): the id of the child it goes just before; the end when not given.copy_itemPOST /api/items/:id/copy(answers 201) — Copies a folder, plan or document into a project's root or one of its folders, whole (text, threads, revisions, Subplans, everything under a folder) with every id new; never the Readme or the Context folder. Arguments:Id(string, required): the item's id;Project(string, required): the project the copy goes into;Parent(string): a folder's id; the root when not given.archive_itemPOST /api/items/:id/archive— Archives a plan with its Subplans: out of the tree into the project's archive, where it is read and never written. Arguments:Id(string, required): the plan's id.restore_itemPOST /api/items/:id/restore— Restores an archived plan, with its Subplans, to the root of its project's tree. Arguments:Id(string, required): the plan's id.list_revisionsGET /api/plans/:id/revisions— The revisions of a plan or document: each with its number, who made it, when and why (an edit, an apply with its thread, an import). Arguments:Id(string, required).read_revisionGET /api/plans/:id/revisions/:n— One revision of a plan or document, with its text as it was. Arguments:Id(string, required);Revision(integer, required): the revision's number.list_threadsGET /api/plans/:id/threads— The threads of a plan, each with its status, its anchor and where it is found, its replies, whom it waits on and whether it waits on you; Status keeps one state (contested, resolved, applied, mine ...). Arguments:Id(string, required);Status(string): a status filter, as Rules.Filter knows them.threadPOST /api/plans/:id/threads(answers 201) — A new thread of yours on a plan: on the passage Anchor ({ Text, Prefix?, Suffix? }, plain words of the text, no markdown marks), or on the whole document without it; Resolve posts it resolved, the comment its outcome (Comment and resolve). Arguments:Id(string, required): the plan's id;Text(string, required): the comment, markdown;Anchor(object): { Text, Prefix?, Suffix? }: the passage; the whole document without it;Resolve(boolean): post it resolved.replyPOST /api/plans/:id/threads/:tid/replies(answers 201) — A reply of yours on a thread; a reply to a settled thread reopens it. Resolve resolves it in the same write, the reply being its outcome (Reply and resolve). Arguments:Id(string, required): the plan's id;Thread(string, required): the thread's id;Text(string, required): the reply, markdown;Resolve(boolean).reanchorPOST /api/plans/:id/threads/:tid/anchor— Moves a thread, detached or not, onto a passage of the current text: Anchor { Text, Prefix?, Suffix? }, plain words found exactly once. Arguments:Id(string, required): the plan's id;Thread(string, required): the thread's id;Anchor(object, required): { Text, Prefix?, Suffix? }.resolvePOST /api/plans/:id/threads/:tid/resolve— Resolves a contested thread, accepting the outcome stated in its last reply; the starter's or an Owner's. A build log resolved with the project's Work options on is committed. Arguments:Id(string, required): the plan's id;Thread(string, required): the thread's id.delete_threadDELETE /api/plans/:id/threads/:tid— Deletes a thread; the owner's. The revisions that applied it keep their text. Arguments:Id(string, required): the plan's id;Thread(string, required): the thread's id.applyPOST /api/plans/:id/threads/:tid/apply— Applies a resolved thread: Outcome says what was decided; Text is the plan's whole new text when it changes, made from Revision (refused when stale); Anchor moves the thread onto the passage the change produced. Answers the thread and the plan. Arguments:Id(string, required): the plan's id;Thread(string, required): the thread's id;Outcome(string, required): what was decided and done, recorded on the thread;Text(string): the whole new text; the outcome alone without it;Revision(integer): the revision Text was made from; required with Text;Anchor(object): { Text, Prefix?, Suffix? }: the passage the thread points at after.waitingGET /api/waiting— Every thread waiting on you, across every plan and project you may see: the contested threads whose last word is not yours, and the resolved ones to apply; each with its plan.list_tasksGET /api/tasks— The tasks a run can be asked for (Answer, Enhance, Work, Explain, Session and any of your own): the server's list, or the ones a project enables with Project; with the Writes levels, the package switches and the shipped defaults. Arguments:Project(string): a project's id: its enabled tasks only.list_llmsGET /api/llms— The server's LLM connections (their public part; the whole records for the owner), each with its running run; the kinds and the default prompts; whether this server runs LLMs at all.create_llmPOST /api/llms(answers 201) — A new LLM connection from the fields given (Name, Kind, Command or Model and Url, the prompts), filled with the defaults and checked; the owner's. Arguments:Name(string);Kind(string): one of the kinds list_llms names.check_llmPOST /api/llms/check— Checks a connection as given, saved or not: runs its command or asks its Ollama once; the owner's. Arguments:Kind(string).update_llmPUT /api/llms/:id— A saved LLM connection changed: the fields given over its record, filled and checked; the owner's. Arguments:Id(string, required): the connection's id.check_saved_llmPOST /api/llms/:id/check— Checks a saved connection, the fields given over its record (the popup's unsaved ones); the owner's. Arguments:Id(string, required): the connection's id.delete_llmDELETE /api/llms/:id— Deletes a saved LLM connection; not while it is running; the owner's. Arguments:Id(string, required): the connection's id.run_listsGET /api/runs/lists— What a run of a connection on a plan could package: the project's other Context documents and the plan's threads waiting on the connection, for the packaging popup. Arguments:Project(string, required);Plan(string);Llm(string): the connection's id.package_runPOST /api/runs/package— The prompt a run would get, without running it: the task's package over the project and the plan, with the person's instructions (Text) and the connection's unsaved fields (Overrides). Arguments:Llm(string, required): the connection's id;Task(string, required): the task's id;Project(string, required);Plan(string);Text(string): the instructions for a task with a box;Overrides(object).start_runPOST /api/runs(answers 201) — Starts a run: the task pressed on a plan (or the project) through a connection, as you, in the project's folder, on a token named after the task that carries its Writes; its record comes back at once (Status running), or queued behind a run of the same plan or connection when there is one (Status queued, Behind which run), to start when its turn comes, packaged afresh; a run that writes files pulls first when the project's GitSync is on. Arguments:Llm(string, required): the connection's id;Task(string, required): the task's id;Project(string, required);Plan(string);Text(string): the instructions for a task with a box.list_runsGET /api/runs— The run records, newest first: of one connection (Llm, with its running one), of a project, a task or an asker (Project, Task, By). Arguments:Llm(string);Project(string);Task(string);By(string): the account that pressed.read_runGET /api/runs/:id— One run's whole record: what it was given, what it answered, its status. Arguments:Id(string, required): the run's id.stop_runPOST /api/runs/:id/stop— Stops a running or queued run; the one who pressed it, or the Administrator. Arguments:Id(string, required): the run's id.delete_runDELETE /api/runs/:id— Removes a run's record; not a running one; the one who pressed it, or the Administrator. Arguments:Id(string, required): the run's id.list_usersGET /api/users— The server's accounts (the owner, the users, the Service accounts), their public part; whether you are the Administrator.create_userPOST /api/users(answers 201) — A new account: a user (Type user, with the email Clerk will match at the first sign-in) or a Service account (Type service); the Administrator's. Arguments:Type(string, required): user or service;Login(string, required);Display(string);Email(string).read_userGET /api/users/:id— An account's page: its record with its token names; your own, or any for the Administrator. Arguments:Id(string, required): the account's id.update_userPUT /api/users/:id— Changes an account: the Administrator its Login, Display and Email, an account its own Display. Arguments:Id(string, required): the account's id;Login(string);Display(string);Email(string).delete_userDELETE /api/users/:id— Deletes an account, out of every project's role folders and its tokens with it; never the owner; the Administrator's. Arguments:Id(string, required): the account's id.mint_tokenPOST /api/users/:id/tokens(answers 201) — Mints a token named Name for an account, its value answered this once; an account mints its own, the Administrator a Service account's. Arguments:Id(string, required): the account's id;Name(string, required): the token's name, unique for the account.revoke_tokenDELETE /api/users/:id/tokens/:name— Revokes an account's token by its name; your own, or any for the Administrator. Arguments:Id(string, required): the account's id;Name(string, required): the token's name.activityGET /api/users/:id/activity— An account's activity on the server, newest first: the threads it opened, its replies, resolves, applies and revisions, each with its project and plan; your own, or any for the Administrator. Arguments:Id(string, required): the account's id.set_rolesPUT /api/projects/:pid/roles— A project's role folders, whole: Owners, Members, Service and Watchers, each a list of account ids, an account in one at most; a person needs the license the folder asks for; an Owner's or the Administrator's. Arguments:Project(string, required): the project's id;Owners(array);Members(array);Service(array);Watchers(array);Version(integer): the project version the change was made from.
Four routes are not operations: GET /api/clerk (everyone, before identify: { On, Required, PublishableKey, FrontendApi, Pricing }, how people sign in through Clerk, never the secret), POST /api/clerk/webhook (Clerk's events, signed with svix-id, svix-timestamp and svix-signature over the raw body; the registry follows them), GET /api/events (below), and POST /api/mcp (below).
MCP
The same operations as tools over the Model Context Protocol, for Codex, Cursor, Claude Desktop, Claude Code and any other client of it: tools/list describes every operation above with its arguments, tools/call performs one as the caller, checked against their role as a request is, posting with the same byline, and answers its JSON as text (a refusal as an error result with the sentence). Two transports, no library:
POST /api/mcp: a JSON-RPC 2.0 message, or a batch, in the body, with the same header a request carries (X-Consensus-Service, orAuthorization: Bearer <token>); the reply as JSON, a notification answered 202 with nothing, a body that is not JSON the protocol's parse error. Nothing is server-initiated:GETanswers 405.node bin/mcp.js --server <url or saved name> [--service <name> | --token <token>] [--project <id or name>]: the stdio transport, a relay on the client's machine to a running server'sPOST /api/mcp, one message per line in and out.--projectfills in the project for the tools that take one when the client leaves it out.
A project is given by its id or its name wherever an operation takes one. Working with an LLM says how a client is configured.
The rules, in plain words
- A thread is contested until its starter or an Owner of the project resolves it (a Member the threads they started; no vote). Resolving accepts the outcome the last reply states.
- A resolved thread is resolved, waiting to be applied, until someone with write access to the project applies it: an Owner, a Member or a Service account, in person or through an LLM acting for them (a Watcher is refused 403). Applying makes it applied and records who, when, the revision and the outcome.
- Any reply to a resolved or applied thread reopens it. Reopening never reverts a change already applied.
- A contested thread waits on its parties, the starter, the project's Owners, everyone who has replied in it and the LLM speakers, except the one who replied last; a person who has not joined in is not waited on. The speaker is the byline, so a run's reply as
Claude-CLI by Useris a different speaker fromUser, and a question the LLM asks you still waits on you. A plan's speakers are the owner, one per LLM connection, the Owners of its project, and everyone seen in its threads. A resolved thread waits on the LLM speakers, the ones that apply it, never on a person. An applied thread waits on nobody. - A plan's state is set by an Owner of the project (the server's owner is one of every project's) at any time, 403 for anyone else, and does not depend on its threads.
- One run per plan at a time, and one per connection: a press while either has a run on is queued behind it, in press order (
POST /api/runsabove). - Editing is allowed at any time and makes a new revision. A manual edit changes no thread's status.
- Anchors hold to the visible text. After every change each anchor is found again; one that cannot be found is detached, and can be re-anchored.
- A text change carries the revision it was made from; a stale one is refused with the current revision.
Live events
GET /api/events is a stream of Server-Sent Events: one event per change, naming the plan or project and the kind of change. The page uses it to stay in step; a client of your own can too.