Skip to content

API reference

Every REST route the Orchestrat server exposes under /api/v1, with authentication, pagination, idempotency, concurrency and error rules.

The Orchestrat server speaks JSON over HTTP. The dashboard, the CLI and the MCP bridge all use the routes on this page. Examples use http://localhost:8801.

Request and response shapes are Zod schemas in packages/protocol; treat them as the source of truth for field-level detail.

Conventions

  • Prefixes. Routes for people live under /api/v1. Routes for agents live under /api/v1/agent. A few public routes need no token; see Public routes.
  • Status codes. Creates return 201 and queued work returns 202. Repository and integration deletes return 204 with no body. Everything else returns 200.
  • IDs are UUIDs, and timestamps are ISO 8601 strings.
  • Request IDs. Every response has an x-request-id header, also echoed in error bodies.
  • Pagination. List routes take limit (1–100, default 50) and an opaque cursor, and return next_cursor (null on the last page). Event lists use after instead of cursor. A malformed cursor returns 400 VALIDATION_ERROR.
  • Optimistic concurrency. Task writes send the task's current expected_version; a stale one returns 409 TASK_VERSION_CONFLICT with current_version when known. Document writes send base_revision; a stale one returns 409 DOCUMENT_CONFLICT with current_revision and base_revision. Successful writes bump the version.
  • Archived projects stay readable; writes return 409 PROJECT_ARCHIVED.

Idempotency keys

Some creates require an Idempotency-Key header of 1–128 characters. Repeating a request with the same key returns the original response. Reusing the key for a different request returns 409 IDEMPOTENCY_CONFLICT. A missing key returns 400 VALIDATION_ERROR.

Keys are scoped to the caller (person or agent). Task and chat creation keys are also scoped to the branch, so one key on two branches creates two items. Merge keys are scoped to the person and the target branch.

The key is required on:

  • task, comment and bulk task creation (people and agents)
  • chat creation and chat messages (people and agents)
  • handoffs (people and agents)
  • POST /api/v1/agent/hooks/lifecycle
  • POST /api/v1/projects/:projectId/merges

Authentication

People

People send an access token on every /api/v1 route except the public ones:

Authorization: Bearer orch_sess_<token>

Sign-up, sign-in and refresh return a session. Shapes are in packages/protocol/src/auth.ts:

{
  "access_token": "orch_sess_…",
  "refresh_token": "orch_refresh_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_expires_in": 2592000,
  "user": {
    "user_id": "…",
    "email": "you@example.com",
    "full_name": "…",
    "is_instance_admin": true
  },
  "invited_project_id": null
}

The access token lasts 1 hour and the refresh token 30 days. Refreshing returns a new pair and retires the old refresh token. Presenting the previous refresh token more than 60 seconds after it was replaced ends the session, because it may have leaked. A session ends 90 days after sign-in, however often it is refreshed. Session responses carry Cache-Control: no-store.

MethodPathAuthBehavior
GET/api/v1/auth/instancenone{ needs_setup, signup_mode }: whether the server has no accounts yet, and invite, open or closed
POST/api/v1/auth/signupnone{ email, password, full_name, invite_token?, setup_token? }; 201 with a session. The first account needs setup_token and becomes the instance admin. invited_project_id is set when an invite was used
POST/api/v1/auth/loginnone{ email, password }; a session
POST/api/v1/auth/refreshnone{ refresh_token }; a new session pair
POST/api/v1/auth/revokenone{ refresh_token }; ends that token's session, for when the access token has expired. 204 whether or not the token was valid
POST/api/v1/auth/logoutsessionEnds the current session; 204
POST/api/v1/auth/logout-allsessionEnds every session of the account, including the current one, and revokes its CLI credentials; 204
POST/api/v1/auth/passwordsession{ current_password, new_password }; 204. Ends every other session and revokes the account's CLI credentials

Emails are trimmed and lowercased, up to 320 characters. Passwords are 10 to 256 characters, and full_name 1 to 120. Sign-up follows ORCHESTRAT_SIGNUP: with invite (the default) it needs a valid, unused invite_token, which the sign-up uses up. See Users and accounts.

CodeStatusWhen
INVALID_CREDENTIALS401Wrong email or password at sign-in
REFRESH_TOKEN_INVALID401The refresh token is unknown, expired, revoked or retired, or the session is too old
SETUP_TOKEN_REQUIRED403The first sign-up has no setup token, or the wrong one
SIGNUP_CLOSED403ORCHESTRAT_SIGNUP is closed
INVITE_REQUIRED403The invite token is missing while ORCHESTRAT_SIGNUP is invite, or it is invalid, used, revoked or expired
PASSWORD_INCORRECT403The current password is wrong when changing it
EMAIL_TAKEN409An account already has that email
RATE_LIMITED429See Rate limits

A missing, expired or revoked access token returns 401 UNAUTHENTICATED. A request for a project you can't access returns 403 PERMISSION_DENIED. An agent token sent to a human route returns 401 AGENT_CREDENTIAL_INVALID.

Agents

Agents call routes under /api/v1/agent with exactly one Authorization header holding an agent token:

Authorization: Bearer orch_agent_<token>
  • An agent token belongs to one agent in one project. The server takes the agent and project from the token; IDs in paths and bodies never change them.
  • Tokens are random, stored only as hashes, and returned once, when issued.
  • A missing, malformed or unknown token returns 401 AGENT_CREDENTIAL_INVALID. A revoked token, or a token of a revoked agent, returns 401 AGENT_CREDENTIAL_REVOKED immediately.
  • Agents name a branch with X-Orchestrat-Branch-Id or pin requests to a session with X-Orchestrat-Session-Id; see Choosing a branch.

Agent tokens are minted in two ways: by the CLI's browser approval (below), or by a project owner or admin through Agents and credentials.

CLI browser approval

orchestrat setup uses these routes to get a CLI credential and then one agent token per integration.

MethodPathAuthBehavior
POST/api/v1/cli/authorization/attemptsnoneStart an attempt; 201 { attempt_id, secret, expires_at }. Attempts expire after 10 minutes
POST/api/v1/cli/authorization/attempts/:attemptId/approvesessionApprove with an empty JSON body, from the dashboard's /cli/authorize?attempt=<attempt_id> page
POST/api/v1/cli/authorization/attempts/:attemptId/denysessionDeny the attempt
POST/api/v1/cli/authorization/attempts/:attemptId/poll{ secret }{ status }: pending, denied, expired, or approved with a one-time credential (orch_cli_…). At most one poll per attempt every 2 seconds, otherwise 429
GET/api/v1/cli/setup/projectsCLI credentialProjects the approving person can access: { projects: [{ id, name, organization_name }] }
GET/api/v1/cli/setup/projectCLI credential{ project_id }: the project the credential is bound to, or null
POST/api/v1/cli/setupCLI credential{ project_id, integration }; creates an agent and returns { project_id, integration, agent, credential }, where credential.token is the agent token. Requires project owner or admin
POST/api/v1/cli/setup/disconnectCLI credential{ project_id, agent_ids, revoke_cli }; revokes 1–5 agents, and the CLI credential itself when revoke_cli is true

integration is one of codex, claude-code, cursor, antigravity or opencode. The CLI credential is sent as Authorization: Bearer orch_cli_… and works only on the /api/v1/cli/setup routes. An invalid one returns 401 UNAUTHENTICATED.

Errors

Errors share one envelope:

{
  "error": {
    "code": "PERMISSION_DENIED",
    "message": "You do not have access to this project.",
    "details": {},
    "request_id": "req_…"
  }
}

Schema validation failures return 400 VALIDATION_ERROR with the validation issues in details; an oversized body returns 413 VALIDATION_ERROR. Some codes add a details.reason, listed per area below.

CodeStatusMeaning
UNAUTHENTICATED401Missing or invalid session, CLI credential, or realtime or download ticket
INVALID_CREDENTIALS, REFRESH_TOKEN_INVALID401Wrong email or password; unusable refresh token. See People
SETUP_TOKEN_REQUIRED, SIGNUP_CLOSED, INVITE_REQUIRED, PASSWORD_INCORRECT403Sign-up and password rules. See People
AGENT_CREDENTIAL_INVALID, AGENT_CREDENTIAL_REVOKED401Bad or revoked agent token
PERMISSION_DENIED403No access, or the role is too low
CLAIM_NOT_OWNER403The claim belongs to another agent or session
NOT_FOUND, AGENT_NOT_FOUND, AGENT_SESSION_NOT_FOUND, CLAIM_NOT_FOUND, BRANCH_NOT_FOUND404The resource doesn't exist or isn't visible to the caller
VALIDATION_ERROR, REPOSITORY_NOT_CONFIGURED400Invalid request, or the Git provider isn't set up on this server
CONFLICT409Generic conflict, often with details.reason; also a duplicate slug or email
EMAIL_TAKEN409An account already has that email
ORGANIZATION_SLUG_TAKEN409Another organization has the slug
PROJECT_ARCHIVED409Write to an archived project
IDEMPOTENCY_CONFLICT409Idempotency key reused for a different request
TASK_VERSION_CONFLICT, TASK_DEPENDENCY_CYCLE409Stale expected_version; dependency would form a cycle
TASK_NOT_CLAIMABLE, TASK_ALREADY_CLAIMED409The task can't be claimed or changed now; another session holds it
AGENT_SESSION_CLOSED, AGENT_SESSION_STALE409The session is closed, or its last heartbeat is more than 60 seconds old
DOCUMENT_CONFLICT, DOCUMENT_EDIT_MISMATCH, DOCUMENT_ARCHIVED409Stale base_revision; an edit's old_text doesn't match exactly once; the document is archived
ATTACHMENT_QUOTA_EXCEEDED, ATTACHMENT_UPLOAD_EXPIRED409Project attachment quota reached; the upload expired
TASK_COMMENTS_UNREAD, WATCH_LIMIT, INVALID_STATUS_TRANSITION, BLOCKER_CYCLE409Attention rules; see Attention and handoffs
BRANCH_ARCHIVED, BRANCH_MISMATCH, BRANCH_NAME_TAKEN, BRANCH_PREVIEW_STALE, BRANCH_DECISION_STALE409Branch rules; see Branch errors
REPOSITORY_AUTHORIZATION_INVALID, REPOSITORY_CONNECTION_EXISTS, INTEGRATION_IN_USE409Repository and integration rules; see Repository connections
RATE_LIMITED, WAIT_LIMIT, HANDOFF_CHAIN_LIMIT, HANDOFF_RATE_LIMITED, WAKE_LIMIT429See Rate limits
INTERNAL_ERROR500Unexpected failure; 502 when a Git provider doesn't answer
DOCUMENT_CONTENT_UNAVAILABLE, STORAGE_UPLOAD_FAILED503Object storage isn't configured or can't be reached. A failed storage call returns 502, and an uploaded size that doesn't match the reservation returns 409

Rate limits

Limits are fixed windows kept in the server's memory, one minute unless the table says otherwise. A limited request returns 429 RATE_LIMITED with a Retry-After header and details.retry_after_seconds.

Limits per IP see the client's address only when the server trusts its reverse proxy; set ORCHESTRAT_TRUST_PROXY_HOPS behind one. See Configuration.

ScopeLimit
POST /api/v1/auth/login20 a minute per IP, and 10 failed attempts per email in 15 minutes
POST /api/v1/auth/signup10 a minute per IP
POST /api/v1/auth/refresh, POST /api/v1/auth/revoke60 a minute per IP, shared
POST /api/v1/auth/password5 per person in 15 minutes
POST /api/v1/cli/authorization/attempts10 a minute per IP
POST /api/v1/cli/authorization/attempts/:attemptId/poll120 a minute per IP. Polling one attempt more often than every 2 seconds also returns 429, without Retry-After
Attachment uploads, downloads and deletes, avatar upload120 a minute per person or agent token
GET /api/v1/avatars/:key1,200 a minute per IP
Webhooks600 a minute per IP
New branches registered by an agent10 a minute per agent token

The attention routes have their own limits: WAIT_LIMIT caps concurrent long-polls, and HANDOFF_RATE_LIMITED, HANDOFF_CHAIN_LIMIT and WAKE_LIMIT stop handoff and wake loops.

Public routes

These routes take no session or agent token.

MethodPathBehavior
GET/health, /api/v1/health{ "status": "ok" } while the process is up
GET/ready, /api/v1/ready{ "status": "ready" } after a database check, or 503 { "status": "not_ready" }
GET/api/v1/attachments/download?ticket=<ticket>Streams an attachment. The ticket comes from a download request, is single-use, and lasts 60 seconds
GET/api/v1/avatars/:keyAn avatar image. The key is random and comes from a profile's avatar_url
GET/api/v1/repository/callback/:providerGit provider OAuth redirect; see Webhooks and provider callbacks
POST/api/v1/webhooks/:providerGit provider webhooks
GET/api/v1/realtime/projects/:projectId?ticket=<ticket>WebSocket upgrade with a realtime ticket; see Realtime

The instance, signup, login, refresh and revoke auth routes are also public (see People), and so are the CLI authorization attempts and poll routes (see CLI browser approval).

When the server serves the dashboard, every other GET outside /api, /health and /ready returns the dashboard's index.html, so client-side routes load directly.

Profile

MethodPathBehavior
GET/api/v1/profileThe signed-in person: user_id, provider, username, full_name, email, avatar_url, is_instance_admin
PATCH/api/v1/profileSet username (3–32 letters, digits or _), full_name and avatar_url; all three are required. avatar_url is null, the current avatar, or an external image URL; only an upload stores a new avatar on the server
POST/api/v1/profile/avatarMultipart upload of a JPEG, PNG or WebP avatar, up to 2 MiB. Requires object storage. Sets avatar_url to /api/v1/avatars/<key> on ORCHESTRAT_PUBLIC_URL, or root-relative when that is unset
DELETE/api/v1/profile/avatarRemove the avatar

Organizations

MethodPathBehavior
GET/api/v1/organizationsYour organizations
POST/api/v1/organizations{ name, slug? }; creates the organization with you as owner
GET/api/v1/organizations/:idOne organization
PATCH/api/v1/organizations/:idUpdate it (owner or admin)
DELETE/api/v1/organizations/:idDelete it (owner). The body must contain its current name
GET/api/v1/organizations/:organizationId/membersList members
PATCH/api/v1/organizations/:organizationId/members/:memberId{ role } (owner or admin, with owner safeguards)
DELETE/api/v1/organizations/:organizationId/members/:memberIdRemove a member (owner or admin, with owner safeguards)
POST/api/v1/organizations/:organizationId/members/leaveLeave. The sole owner can't leave
GET/api/v1/organizations/:organizationId/projectsProjects you can access in the organization
GET/api/v1/organizations/:organizationId/project-summariesThe same projects with your role, default branch, member and open-task counts, last activity and repository
POST/api/v1/organizations/:organizationId/projectsCreate a project (any organization member); you become its owner

Roles are owner, admin and member. Slugs are optional and derived from the name when omitted.

Projects

MethodPathBehavior
GET/api/v1/projects/:idOne project, with default_branch_id
PATCH/api/v1/projects/:idUpdate name, slug or description (owner or admin)
POST/api/v1/projects/:id/archiveArchive, idempotently (owner or admin)
POST/api/v1/projects/:id/reactivateReactivate, idempotently (owner or admin)
DELETE/api/v1/projects/:idDelete (owner). The body must contain its current name

Project creation accepts name, optional slug, description and task_key_prefix (2–8 letters or digits, uppercased). Without a prefix, the server uses the first three alphanumeric characters of the slug when at least two exist, otherwise PRJ.

Members and invitations

Access to a project needs membership in both the organization and the project.

MethodPathBehavior
GET/api/v1/projects/:projectId/membersList project members
POST/api/v1/projects/:projectId/members{ user_id, role? }; add an existing organization member (owner or admin)
PATCH/api/v1/projects/:projectId/members/:userId{ role } (owner or admin; only owners change owners)
DELETE/api/v1/projects/:projectId/members/:userIdRemove a member (owner or admin; the last owner can't be removed)
GET/api/v1/projects/:projectId/invitationsList invitation links (owner or admin)
POST/api/v1/projects/:projectId/invitationsCreate a link; 201 { invitation, token }. The token is shown once and lasts 7 days
POST/api/v1/projects/:projectId/invitations/:invitationId/revokeRevoke a link (owner or admin)
POST/api/v1/project-invitations/accept{ token }; joins the organization and the project as member

Tasks

MethodPathBehavior
POST/api/v1/projects/:projectId/tasksCreate a task. Idempotency-Key required
GET/api/v1/projects/:projectId/tasksList task cards with filters and cursor pagination
GET/api/v1/tasks/:idOne task card
PATCH/api/v1/tasks/:idUpdate title, description or priority with expected_version
POST/api/v1/tasks/:id/moveMove to a status and position with expected_version
GET/api/v1/tasks/:id/commentsList comments with cursor pagination
POST/api/v1/tasks/:id/commentsAdd a Markdown comment. Idempotency-Key required
GET/api/v1/tasks/:id/dependenciesTasks this task depends on, and tasks it blocks
POST/api/v1/tasks/:id/dependenciesAdd a dependency with expected_version
DELETE/api/v1/tasks/:id/dependencies/:dependsOnTaskIdRemove a dependency with expected_version
  • Fields. id, project_id, branch_id, key, title, description, priority, status, position, version, created_by, created_by_user_id, created_at, updated_at, started_at, completed_at, cancelled_at. Cards add comment_count and unresolved_blocker_count. created_by says whether a person or an agent created the task; the legacy created_by_user_id is null for agent-created tasks.
  • Values. Priorities: urgent, high, medium, low, none. Statuses: backlog, ready, claimed, in_progress, blocked, review, done, cancelled. People can't select claimed; only the agent claim flow sets it.
  • Create takes title, optional description, priority (default none) and status (default backlog). The task is appended to its status column.
  • List takes status, priority, limit and cursor, ordered by position then ID. It returns { tasks, total, next_cursor }, where total counts every match on the branch.
  • Move takes status, optional before_task_id (an anchor in the target column) and expected_version. Entering in_progress, done or cancelled sets the matching timestamp; leaving the status clears it.
  • Comments are immutable Markdown in body_markdown, with a per-task sequence and up to 20 mentions. Reads return { comments, next_cursor }.
  • Dependencies stay on one branch: a dependency on another branch's task returns 400 VALIDATION_ERROR with reason: "cross_branch". A self-edge or cycle returns 409 TASK_DEPENDENCY_CYCLE.

Agents and credentials

MethodPathBehavior
POST/api/v1/projects/:projectId/agents{ name }; create an agent (owner or admin)
GET/api/v1/projects/:projectId/agentsList agents
POST/api/v1/projects/:projectId/agents/:agentId/revokeRevoke an agent with its credentials, sessions and claims (owner or admin)
POST/api/v1/projects/:projectId/agents/:agentId/credentialsIssue an agent token; { credential, token }. The token is returned only here (owner or admin)
GET/api/v1/projects/:projectId/credentialsList credentials, without token values
POST/api/v1/projects/:projectId/credentials/:credentialId/revokeRevoke a credential (owner or admin)

Presence and events

MethodPathBehavior
GET/api/v1/projects/:projectId/presenceEvery active agent: online or offline, current task, session branch_id
GET/api/v1/projects/:projectId/eventsOrdered project events; limit, after, branch_id, all_branches
  • An agent is online when it has an active session with a heartbeat in the last 60 seconds. Its branch_id is its session's branch, or null when offline.
  • Events are ordered by numeric event ID. Pass the previous response's next_cursor as after.
  • Each event has a branch_id, or null for project-wide events (branches, repository, agents, credentials). A branch's list includes its own events plus the project-wide ones. all_branches=true returns every branch; the dashboard's realtime catch-up uses it to keep one cursor.

Documents

MethodPathBehavior
GET/api/v1/projects/:projectId/documentsList documents; limit, cursor, include_archived
POST/api/v1/projects/:projectId/documents{ path, content }; create a document at revision 1
POST/api/v1/projects/:projectId/document-folders{ path }; create a folder
GET/api/v1/documents/:idRead a document and its current revision
PATCH/api/v1/documents/:id{ content, base_revision }; create a revision
POST/api/v1/documents/:id/edits{ base_revision, edits: [{ old_text, new_text }] }; exact text replacements
PATCH/api/v1/documents/:id/path{ path, expected_revision }; rename
POST/api/v1/documents/:id/archiveArchive
POST/api/v1/documents/:id/restoreRestore
GET/api/v1/documents/:id/revisionsList immutable revisions, in revision order
GET/api/v1/documents/:id/revisions/:revisionRead one revision
  • Paths are relative POSIX paths of 1–1024 characters ending in .md, with no leading slash, backslash, NUL, empty segment, or ./.. segment. Paths are unique per branch.
  • Content is Markdown text up to 1,000,000 characters.
  • Writes create one immutable revision and advance current_revision atomically. A stale base_revision returns 409 DOCUMENT_CONFLICT and writes nothing:
{
  "error": {
    "code": "DOCUMENT_CONFLICT",
    "message": "Document revision is stale.",
    "details": { "current_revision": 2, "base_revision": 1 },
    "request_id": "req_…"
  }
}
  • Edits apply up to 1,000 exact replacements in order. Each old_text must match exactly once, otherwise 409 DOCUMENT_EDIT_MISMATCH with edit_index and match_count.
  • Lists are ordered by update time, then ID.

Create and update responses contain the document and its new revision:

{
  "document": {
    "id": "…",
    "project_id": "…",
    "path": "docs/readme.md",
    "current_revision": 1,
    "created_by": { "type": "human", "id": "…" },
    "created_at": "2026-10-14T00:00:00.000Z",
    "updated_at": "2026-10-14T00:00:00.000Z"
  },
  "revision": {
    "id": "…",
    "document_id": "…",
    "revision_number": 1,
    "content": "# Notes",
    "author": { "type": "human", "id": "…" },
    "created_at": "2026-10-14T00:00:00.000Z"
  }
}

Attachments

Attachments need object storage; without it, uploads and downloads return 503 STORAGE_UPLOAD_FAILED.

MethodPathBehavior
GET/api/v1/documents/:documentId/attachmentsList a document's attachments
POST/api/v1/documents/:documentId/attachments/uploads{ filename, content_type?, size_bytes }; start an upload
GET/api/v1/documents/:documentId/attachments/uploads/:uploadIdInspect an upload, including completed parts
POST/api/v1/documents/:documentId/attachments/uploads/:uploadId/refreshExtend the upload and re-sign its URLs
POST/api/v1/documents/:documentId/attachments/uploads/:uploadId/parts{ part_numbers } (up to 100); presigned part URLs
POST/api/v1/documents/:documentId/attachments/uploads/:uploadId/complete{ parts }; verify and finish the upload
DELETE/api/v1/documents/:documentId/attachments/uploads/:uploadIdAbort an upload
POST/api/v1/documents/:documentId/attachments/:attachmentId/download{ download_url }: a single-use URL valid for 60 seconds
DELETE/api/v1/documents/:documentId/attachments/:attachmentIdDelete an attachment

The server never receives upload bytes. Starting an upload returns either a single presigned upload_url or a multipart upload with part_size_bytes; the client sends bytes straight to the bucket, then calls complete. Uploads expire after 24 hours unless refreshed. Files are limited to 2 GiB, and a project's attachments count toward its quota (409 ATTACHMENT_QUOTA_EXCEEDED). Downloads stream through the server from /api/v1/attachments/download.

Chats

MethodPathBehavior
GET/api/v1/projects/:projectId/message-candidatesPeople and agents you can message
GET/api/v1/projects/:projectId/chatsChats on the branch
POST/api/v1/projects/:projectId/chats{ kind, name?, participants }; direct or group (groups need a name). Idempotency-Key required
GET/api/v1/projects/:projectId/inboxUnread chats with unread and mention counts; all_branches=true covers every branch
GET/api/v1/chats/:idOne chat
POST/api/v1/chats/:id/members{ action, participant }; add or remove a participant
POST/api/v1/chats/:id/joinJoin a group chat that has no person in it yet
GET/api/v1/chats/:id/messagesafter or before (a sequence), unread_only, limit
POST/api/v1/chats/:id/messages{ body_markdown, mentions?, document_ids? }. Idempotency-Key required
POST/api/v1/chats/:id/read{ through_message_id }; advance your read position

Participants are { type, id } with type human or agent. A message holds up to 100 mentions and 20 document references; documents must be on the chat's branch.

Attention and handoffs

MethodPathBehavior
GET/api/v1/projects/:projectId/attentionYour attention summary across every branch
GET/api/v1/projects/:projectId/attention/tasksPer-task overlay: claim, block and unread counts
GET/api/v1/projects/:projectId/attention/agentsPer agent: waiting since, pending directed items, open block, current task
POST/api/v1/tasks/:id/watch{ watching }; false mutes the task
POST/api/v1/tasks/:id/comments/read{ through_sequence }
POST/api/v1/tasks/:id/block/resolveResolve a block
POST/api/v1/projects/:projectId/handoffs{ to, body_markdown, task_id? }. Idempotency-Key required
  • A handoff is a direct message to to with an optional task reference. Its body is up to 4,000 characters.
  • An agent can't complete or release a claim, or move its task to review or done, while comments by others posted since the claim are unacknowledged: 409 TASK_COMMENTS_UNREAD. Sending acknowledged_through (a comment sequence) clears it. Imported comments never count.
  • Attention events: task.status_changed, task.block_resolved, chat.handoff_created (its entity is the chat, so chat access rules apply), and run.woken. task.comment_created adds sequence and mentions; claim.released adds reason.

Agent routes are under Agent attention and handoffs. How attention works: Architecture.

Branches

Every project has branches, and exactly one is the default. Tasks (with comments, dependencies and claims), documents (with folders, revisions and attachments), chats (with participants and messages) and agent sessions belong to one branch. Organizations, projects, members, agents and credentials are project-wide. Project responses include default_branch_id. How branches work: Architecture.

Choosing a branch

A request that names no branch uses the default branch, so clients written before branches keep working.

People. These routes take an optional branch_id query parameter:

  • GET and POST /api/v1/projects/:projectId/tasks
  • GET and POST /api/v1/projects/:projectId/documents
  • POST /api/v1/projects/:projectId/document-folders
  • GET and POST /api/v1/projects/:projectId/chats
  • GET /api/v1/projects/:projectId/inbox
  • GET /api/v1/projects/:projectId/events
  • POST /api/v1/projects/:projectId/handoffs

A branch_id outside the project returns 404 BRANCH_NOT_FOUND. Archived branches stay readable; a create on one returns 409 BRANCH_ARCHIVED. Routes for a single task, document, attachment or chat take the branch from that item. Presence, message candidates and the human attention routes are project-wide.

Agents. Agent routes resolve the branch in this order:

  1. Pinned session. X-Orchestrat-Session-Id: <session id> uses that session's branch. A claim uses the session in its body. If X-Orchestrat-Branch-Id names a different branch, the request fails with 409 BRANCH_MISMATCH. A session that isn't the agent's returns 404 AGENT_SESSION_NOT_FOUND.
  2. Header. X-Orchestrat-Branch-Id: <branch id>.
  3. The default branch.

Both headers must be UUIDs; a malformed value returns 400 VALIDATION_ERROR with details.header. Agents can read tasks, documents and chats on any branch by ID. A write to an item on a different branch than the one named or pinned returns 409 BRANCH_MISMATCH; a write with no branch signal uses the item's branch.

New branch error codes go only to requests that name a branch (?branch_id=, X-Orchestrat-Branch-Id or X-Orchestrat-Session-Id) and to the branch, merge, decision and repository routes. Other requests get an existing code with a details.reason:

FailureResponse
Claim a task on another branch than the session409 TASK_NOT_CLAIMABLE, reason: "branch_mismatch"
Claim a task on an archived branch409 TASK_NOT_CLAIMABLE, reason: "branch_archived"
Write to an item on an archived branch409 CONFLICT, reason: "branch_archived"
Reference an item on another branch400 VALIDATION_ERROR, reason: "cross_branch"

Cross-branch references are task dependencies, blocking tasks, the task in a handoff, and document_ids in chat messages.

Branch routes

MethodPathWhoBehavior
GET/api/v1/projects/:projectId/branchesmemberActive branches, default first, then most recently updated. include_archived=true; name filters by exact name
POST/api/v1/projects/:projectId/branchesmember{ name }; 201 { branch }. 409 BRANCH_NAME_TAKEN when an active branch has the name
GET/api/v1/projects/:projectId/branches/:branchIdmember{ branch }
POST/api/v1/projects/:projectId/branches/:branchId/defaultowner/adminMake it the default. 409 BRANCH_ARCHIVED for an archived branch
POST/api/v1/projects/:projectId/branches/:branchId/archiveowner/adminArchive: read-only, hidden by default, nothing deleted, agent sessions closed. 409 CONFLICT, reason: "default_branch" for the default
POST/api/v1/projects/:projectId/branches/:branchId/onboardingmember{ "choice": "blank" } finishes the branch's setup. 409 CONFLICT, reason: "onboarding_completed" when already done

A branch is { id, project_id, name, is_default, name_source, created_via, created_by, onboarding_state, onboarding_choice, archived_at, archived_reason, provider, open_decision_count, created_at, updated_at }. provider is null without a connected repository, otherwise { provider, state, head_sha } with state tracked, missing or untracked. archived_reason is provider_deleted, manual or replaced_on_connect.

Branch names are 1–255 characters and follow a subset of git check-ref-format: no spaces, control characters or ~ ^ : ? * [ \; no leading - or /; no trailing / or .lock; no .., // or @{; and not @. Names are unique among active branches only.

Imports, merges and decisions

People only. Agents can't archive, merge, import, set the default or resolve decisions.

MethodPathWhoBehavior
POST/api/v1/projects/:projectId/merges/previewmember or owner/admin{ source_branch_id, target_branch_id, kind, selection? }; { preview }
POST/api/v1/projects/:projectId/mergesmember or owner/adminIdempotency-Key required. { source_branch_id, target_branch_id, kind, selection?, choices, preview_fingerprint, archive_source?, complete_onboarding? }; 201 { run }
GET/api/v1/projects/:projectId/mergesmemberbranch_id (source or target), limit, cursor; { runs, next_cursor }, newest first
GET/api/v1/projects/:projectId/decisionsmemberbranch_id (target), status; { decisions }, newest first
POST/api/v1/projects/:projectId/decisions/:decisionId/resolveowner/admin{ choice } with use_source or use_target; { decision }
  • Who. kind is import or merge. Members import into any branch except the default. A merge, an import into the default branch, and archive_source: true need an owner or admin, for the preview as well as the apply.
  • Selection. An import takes { tasks, documents, chats }, each "all" or a list of up to 2,000 lineage keys. A merge always takes everything. Selecting a task also brings the tasks it depends on that the target lacks.
  • Preview. Each item has an outcome: copy, apply, unchanged, keep_target, conflict or skipped, plus its fields and both sides' values. counts totals outcomes per item type, and fingerprint covers every version the apply depends on.
  • Apply. The apply re-plans under the project lock. If the fingerprint no longer matches preview_fingerprint, it returns 409 BRANCH_PREVIEW_STALE with the new preview in details.preview. An import must choose use_source or use_target in choices for every conflict, otherwise 400 VALIDATION_ERROR with reason: "missing_choice".
  • Merges never ask. A conflict keeps the target's version and becomes a decision. A task with an active claim on the target is never changed.
  • Limits. A run takes at most 2,000 top-level items, 20,000 comments and messages, and 5 GiB of attachments (400 VALIDATION_ERROR, reason: "too_large"). The source and target must differ (reason: "same_branch"). Copied attachments count toward the project quota and need object storage.
  • Events. A run emits one merge.import_applied or merge.merge_applied event on the target branch, plus decision.opened when it created decisions.
  • Resolving. use_target keeps the target and records the source state as agreed. use_source applies the recorded source values if the target hasn't changed since; otherwise 409 BRANCH_DECISION_STALE with the refreshed decision in details.decision. use_source on a claimed task returns 409 TASK_ALREADY_CLAIMED, and a settled decision returns 409 CONFLICT with reason: "decision_closed".

A run has trigger dashboard or provider_merge, and a fork merge is recorded with status: "skipped" and skip_reason: "fork". A decision has reason changed_on_both, added_on_both, path_collision or target_claimed, and status open, resolved_source, resolved_target or superseded.

Branch errors

CodeStatusWhen
BRANCH_NOT_FOUND404The branch isn't a branch of this project
BRANCH_ARCHIVED409A write names an archived branch: opening a session there, making it the default, importing or merging into it; or register names a branch the provider deleted
BRANCH_MISMATCH409The named branch differs from the pinned session's, or an agent writes to an item on another branch
BRANCH_NAME_TAKEN409An active branch already has the name
BRANCH_PREVIEW_STALE409The branches changed since the preview
BRANCH_DECISION_STALE409The target changed since the decision was recorded

Existing codes gain these details.reason values:

CodeReasons
400 VALIDATION_ERRORcross_branch, same_branch, too_large, missing_choice, default_branch
409 CONFLICTbranch_archived, default_branch, onboarding_completed, decision_closed, path_taken, no_hook
409 TASK_NOT_CLAIMABLEbranch_mismatch, branch_archived
409 AGENT_SESSION_CLOSEDbranch_mismatch

Repository connections

A project can connect one repository. provider is github, gitlab or origin (Cursor Origin, hidden unless the server enables it). Set providers up first; see Git providers.

MethodPathWhoBehavior
GET/api/v1/repository/providerssigned in{ providers }, each with configured and unavailable_reason
GET/api/v1/repository/authorizations/pending?provider=<provider>signed inYour newest authorized attempt: { authorization_id, project_id, provider, expires_at }, or 404. GitHub's Setup URL can open in another browser, so the dashboard looks the attempt up here
GET/api/v1/projects/:projectId/repositorymember{ repository, providers }; repository is the active connection or null
POST/api/v1/projects/:projectId/repository/authorizationsowner/admin{ provider, return_path? }; { authorization_id, authorize_url, expires_at }, valid 15 minutes
POST/api/v1/projects/:projectId/repository/authorizations/:authorizationId/github-installationowner/admin, same person{ installation_id } from GitHub's Setup URL (untrusted, checked against your GitHub token)
GET/api/v1/projects/:projectId/repository/authorizations/:authorizationId/repositoriesowner/admin, same person{ repositories, install_url }, each with eligible and ineligible_reason
POST/api/v1/projects/:projectId/repositoryowner/admin, same person{ authorization_id, provider_repository_id, default_branch_mapping, create_existing_branches }; 201 { repository }
POST/api/v1/projects/:projectId/repository/importowner/admin of the project and the organization{ repository_id }; connect a repository from the organization's integrations; 201
POST/api/v1/projects/:projectId/repository/syncowner/adminQueue a reconcile; 202 { "queued": true }
POST/api/v1/projects/:projectId/repository/hook/reenableowner/adminGitLab: wake a disabled project hook; 202. Other providers: 409 CONFLICT, reason: "no_hook"
GET/api/v1/projects/:projectId/repository/deliveriesowner/adminOptional status; the newest 100 webhook deliveries
POST/api/v1/projects/:projectId/repository/deliveries/:deliveryId/retryowner/adminPut a failed delivery back in line; { delivery }
DELETE/api/v1/projects/:projectId/repositoryowner/adminDisconnect; 204. Branches and their work are kept. For GitLab, the hook is deleted and tokens revoked

default_branch_mapping links the project's default branch to the provider's default branch D:

  • { "mode": "rename_default" } (the default) renames the default branch to D. Another active branch named D is archived with reason replaced_on_connect; its work stays importable.
  • { "mode": "use_existing", "branch_id": "…" } makes the active branch named D the default; the old default stays an ordinary branch.

create_existing_branches (default false) also creates branches for the provider's other branches; otherwise each one gets a branch on its first push.

Sync, re-enable, retry and disconnect return 404 NOT_FOUND when no repository is connected. A provider that doesn't answer gives 502 INTERNAL_ERROR, and lost provider access gives 409 REPOSITORY_AUTHORIZATION_INVALID. There is no reauthorization route: disconnect and connect again, and branches and their work are kept.

CodeStatusWhen
REPOSITORY_NOT_CONFIGURED400The provider isn't set up on this server
REPOSITORY_AUTHORIZATION_INVALID409The authorization is missing, used, expired or another person's; the installation isn't yours; access was revoked; or the repository can't be connected
REPOSITORY_CONNECTION_EXISTS409The project already has a repository, or the repository is connected to another project
INTEGRATION_IN_USE409A GitLab organization integration still has imported projects using it

Organization Git integrations

Organization owners and admins can connect GitHub or GitLab once and import repositories as projects.

MethodPathBehavior
GET/api/v1/organizations/:organizationId/integrationsList the organization's integrations
POST/api/v1/organizations/:organizationId/integrations/authorizations{ provider, return_path? } with github or gitlab; starts the provider's authorization
GET/api/v1/organizations/:organizationId/integrations/authorizations/:authorizationIdGitHub installations you can use, or GitLab projects you maintain, plus install_url for GitHub
POST/api/v1/organizations/:organizationId/integrationsGitHub: { provider, authorization_id, installation_id }. GitLab: { provider, authorization_id, repository_selection, selected_repository_ids? }; 201
PATCH/api/v1/organizations/:organizationId/integrations/:integrationIdGitLab only: { repository_selection, selected_repository_ids } (GitHub's selection changes on GitHub)
DELETE/api/v1/organizations/:organizationId/integrations/:integrationIdRemove; 204. Refused with 409 INTEGRATION_IN_USE while imported GitLab projects use it
POST/api/v1/organizations/:organizationId/integrations/:integrationId/refreshRe-read the integration's repositories now
GET/api/v1/organizations/:organizationId/repositoriesfilter=importable (default: not connected and not dismissed) or all
POST/api/v1/organizations/:organizationId/repositories/:repositoryId/dismissHide a repository from the importable list
POST/api/v1/organizations/:organizationId/projects/import{ repository_id, name, description?, task_key_prefix? }; 201 { project, repository }. If connecting fails, the new project is removed

repository_selection is all or selected.

Webhooks and provider callbacks

MethodPathPurpose
GET/api/v1/repository/callback/:providerProvider redirect for github, gitlab or origin. Always a 302 to the dashboard, with repository_authorization=<id> or repository_error=invalid_state, expired or authorization_failed
POST/api/v1/webhooks/githubGitHub App webhooks
POST/api/v1/webhooks/gitlab/:connectionIdGitLab project hook, one per connection
POST/api/v1/webhooks/originCursor Origin webhooks

Webhooks are verified on the raw body (up to 10 MiB) and stored for the sync worker; nothing changes until the worker applies them. They answer:

  • 202 when the delivery is stored, is a duplicate, or is valid but for no connected repository (stored as ignored)
  • 401 for a bad signature or timestamp
  • 404 when the provider isn't configured, or the GitLab connection is unknown or disconnected
  • 503 when the database is unavailable

See Architecture for how the worker applies deliveries.

Realtime

POST /api/v1/realtime/tickets   { "project_id": "…" }
  -> { "ticket": "…", "expires_at": "…" }
GET  ws://localhost:8801/api/v1/realtime/projects/<projectId>?ticket=<ticket>
  • A ticket needs a session token and project membership. It is single-use, bound to one project, and valid for 45 seconds. Behind TLS, use wss://.
  • The socket sends one envelope per event: project_id, branch_id (when the event has one), sequence, event_type, entity_type, entity_id, and actor. Chat events can add message_id, mentions and affected_participant.
  • entity_type is one of chat, project, task, claim, agent, credential, session, document, hook, run, branch, merge, decision or repository.
  • The socket is server-to-client only; a client message closes it. After a reconnect, read GET /api/v1/projects/:projectId/events with after set to the last sequence you saw.

Agent API

Every route in this section is under /api/v1/agent and needs an agent token. Paths below are relative to that prefix.

Agent sessions and events

MethodPathBehavior
GET/projectThe token's project, with default_branch_id
POST/sessions{ runtime, integration, external_session_id?, branch_id? }; open a session
POST/sessions/:sessionId/heartbeatRefresh the session and renew its active claim leases
POST/sessions/:sessionId/closeClose the session and revoke its active claims
POST/hooks/lifecycle{ event, session_id, runtime, integration, external_session_id?, error? }. Idempotency-Key required
GET/presencePresence for the project (backs the MCP list_agents tool)
GET/eventsProject events; limit, after
  • A session opens on branch_id, else the header's branch, else the default branch, and returns its branch_id. Opening one on an archived branch returns 409 BRANCH_ARCHIVED.
  • A session is stale when its last heartbeat is more than 60 seconds old. Heartbeats and claims on a stale session return 409 AGENT_SESSION_STALE; closing is still allowed.
  • Lifecycle event is session.started, session.ended or run.failed. The branch travels in X-Orchestrat-Branch-Id. session.started for a session that already exists on another branch returns 409 AGENT_SESSION_CLOSED with reason: "branch_mismatch". session.ended also closes the agent's other active sessions with the same external_session_id.

Agent tasks and claims

MethodPathBehavior
GET/tasksList tasks; status, priority, limit, cursor
POST/tasksCreate a backlog or ready task. Idempotency-Key required
POST/tasks/bulk{ tasks }: create 1–50 tasks in one all-or-nothing batch. Idempotency-Key required
PATCH/tasks/bulk{ tasks }: edit or move 1–50 distinct tasks (each with task_id) in one all-or-nothing batch
GET/tasks/:idOne task
PATCH/tasks/:idEdit metadata or move an unclaimed task, with expected_version
GET/tasks/:id/commentsRead comments from people and agents
POST/tasks/:id/commentsAdd a Markdown comment. Idempotency-Key required
POST/tasks/:id/claim{ session_id, lease_seconds? }; claim a ready task
POST/sessions/:sessionId/claims/release{ claim_token, acknowledged_through? }; return the task to ready
POST/sessions/:sessionId/claims/complete{ claim_token, acknowledged_through? }; move the task to done
POST/sessions/:sessionId/claims/statusSet in_progress, blocked or review on an owned claim, using its claim token
  • Claims need an active session on the task's branch, a ready task and resolved dependencies. lease_seconds defaults to 300 and accepts 30–86,400. The response holds the claim and a claim_token; only the owning agent and session can release or complete it. Expired leases are reconciled when tasks are read or claimed. Concurrent claims produce one winner; the other gets 409 TASK_ALREADY_CLAIMED with the active claim's lease details.
  • Updates take title, description, priority, status, optional acknowledged_through and required expected_version, applied as one version bump. A status change follows the human move rules and is refused with 409 TASK_NOT_CLAIMABLE while the task has an active claim: reason is own_claim when the caller holds it (use the claim routes instead), otherwise claimed. Moving to review or done applies the unread-comments guard.
  • Bulk routes run in one transaction: any failure applies nothing and returns the single-item error with details.index and the item's task_id (update) or title (create). Results come back in input order, and each task emits the same events as the single route.

Agent documents and attachments

MethodPathBehavior
GET/documentsList documents; limit, cursor, include_archived
POST/documentsCreate a document at revision 1
GET/documents/:idRead a document
PATCH/documents/:idCreate a revision with base_revision
POST/documents/:id/editsApply exact text edits
PATCH/documents/:id/pathRename
POST/documents/:id/archiveArchive
POST/documents/:id/restoreRestore
GET/documents/:id/revisionsList revisions
GET/documents/:id/revisions/:revisionRead one revision

Bodies and rules match the human Documents routes. Attachment routes mirror the human Attachments routes under /documents/:documentId/attachments. Agents can't create folders.

Agent chats

MethodPathBehavior
GET/message-candidatesPeople and agents the agent can message
GET/chatsThe agent's chats on the resolved branch
POST/chatsCreate a chat. Idempotency-Key required
GET/inboxUnread chats on the resolved branch
GET/chats/:idOne chat
POST/chats/:id/membersAdd or remove a participant
GET/chats/:id/messagesRead messages
POST/chats/:id/messagesSend a message. Idempotency-Key required
POST/chats/:id/readAdvance the read position

Bodies match the human Chats routes.

Agent attention and handoffs

MethodPathBehavior
GET/attentionSummary: unread_chats, watched tasks with unread comments and mentions, pending handoffs, resolved blockers, and a cursor
POST/attention/acknowledge{ items } (1–50): acknowledge task_comments, handoff or blocker items
GET/attention/waitLong-poll: after cursor, timeout_ms (0–25,000, default 25,000; 0 polls), directed_only, kinds. Returns pending, updates or timeout
POST/tasks/:id/watch{ watching }; false mutes the task
POST/handoffs{ to, body_markdown, task_id? }. Idempotency-Key required
POST/wakes{ integration, handoff_id }; the local waker records a wake against the server's budget

Agent branches

MethodPathBehavior
GET/branchesinclude_archived, name; { branches }
GET/branches/:branchId{ branch }
POST/branches/register{ name, git_default_branch?, head_sha?, force_new? }; { branch, created }. Returns the active branch with that name, or creates it

register refuses to revive a branch the provider deleted: when only archived branches have the name and the newest was deleted on the provider, it returns 409 BRANCH_ARCHIVED with branch_id and archived_at, and force_new: true creates a new branch anyway. git_default_branch is the checkout's origin/HEAD branch; it renames the default branch when that is still the fallback main, no repository is connected, and no active branch has the name. Creating more than 10 branches a minute per agent token returns 429 RATE_LIMITED; registering an existing branch is never limited.

Not implemented

The API has no routes for:

  • deleting tasks, or editing and deleting comments
  • remote MCP or MCP OAuth
  • agent capability matching or auto-assignment
  • syncing documents with a local filesystem, or collaborative cursors