Last updated: 2026-03-22
This doc is the route-level contract for OrbitDock's server. It covers every current HTTP endpoint plus the WebSocket entrypoint.
It was audited against:
orbitdock-server/crates/server/src/app/mod.rsorbitdock-server/crates/server/src/transport/http/router.rsOrbitDock/OrbitDock/Services/Server/APIClient.swift
For architecture, ownership, and implementation details, see docs/server-architecture.md.
- Use HTTP REST for reads, mutations, and fire-and-forget actions.
- Use WebSocket (
/ws) for subscriptions, real-time session interaction, and server-pushed events. - New clients should default to REST. Use WebSocket when the operation needs the persistent connection.
- REST mutations often still produce WebSocket broadcasts so other clients stay in sync.
Legacy WebSocket request/response helpers that now map to REST-only routes return:
{
"type": "error",
"code": "http_only_endpoint",
"message": "Use REST endpoint GET /api/... for this request"
}orbitdock init auto-provisions a local auth token — the hash is stored in the database and the plaintext is encrypted in hook-forward.json. Once provisioned, all routes except GET /health require:
Authorization: Bearer <token>Retrieve the local token with orbitdock auth local-token.
Fire-and-forget endpoints usually return:
{"accepted": true}HTTP API errors use:
{
"code": "string_code",
"error": "human message"
}Response:
{"status":"ok"}Returns Prometheus-style metrics text.
Internal hook ingestion endpoint used by orbitdock hook-forward <type>.
Request:
- JSON hook payload
- Includes the injected
typefield (claude_session_start,claude_status_event,claude_tool_event, and so on)
Response:
200 OKwith a JSON acknowledgement payload
Notes:
- This is for Claude hook forwarding, not normal client traffic.
Returns session summaries.
Response:
{
"sessions": [
{
"id": "od-...",
"provider": "codex",
"project_path": "/Users/.../repo",
"status": "active",
"work_status": "waiting",
"active_worker_count": 1,
"pending_tool_family": "shell",
"forked_from_session_id": "od-parent"
}
]
}Notes:
- Session summaries and list items now include
active_worker_count,pending_tool_family, andforked_from_session_id.
Returns aggregated conversation records for the dashboard view. This is a higher-level view than GET /api/sessions, grouping sessions by conversation history.
Response: array of DashboardConversationItem objects.
Notes:
preview_text,activity_summary, andalert_contextare server-derived plain-text summaries for dashboard cards.- Clients should treat them as the shared semantic source of truth and only apply layout-specific presentation on top.
Returns full session state.
Query params:
include_messagesoptional, defaultfalse
Notes:
- Despite the legacy query name, this endpoint returns typed conversation rows in
session.rowswheninclude_messages=true. - When
include_messages=false, the session payload is returned without hydrated row history.
Response:
{
"session": {
"id": "od-...",
"provider": "codex",
"status": "active",
"work_status": "waiting",
"revision": 123,
"rows": [],
"total_row_count": 0,
"has_more_before": false
}
}Error responses:
404 not_found500 db_error503 runtime_error
Returns the bootstrap payload for a conversation view. Includes the shared SessionState plus the first page of typed ConversationRow entries.
Query params:
limitoptional, clamped to1...200, default50before_sequenceoptional, paginates backwards by sequence number
Response:
{
"session": {
"id": "od-...",
"rows": [
{
"session_id": "od-...",
"sequence": 1,
"turn_id": "turn-1",
"row": {
"row_type": "user",
"id": "msg-1",
"content": "Review the approval flow",
"turn_id": "turn-1",
"timestamp": "2026-03-13T12:00:00Z",
"is_streaming": false,
"images": []
}
},
{
"session_id": "od-...",
"sequence": 2,
"turn_id": "turn-1",
"row": {
"row_type": "assistant",
"id": "msg-2",
"content": "Looking at the approval flow now",
"turn_id": "turn-1",
"timestamp": "2026-03-13T12:00:01Z",
"is_streaming": true
}
}
],
"total_row_count": 120,
"has_more_before": true,
"oldest_sequence": 71,
"newest_sequence": 120
},
"total_row_count": 120,
"has_more_before": true,
"oldest_sequence": 71,
"newest_sequence": 120
}See docs/conversation-contracts.md for the full row type reference.
Notes:
- The top-level response is flattened:
session,total_row_count,has_more_before,oldest_sequence, andnewest_sequence. - Native clients use this as the single session bootstrap read, then subscribe detail/composer/conversation surfaces from
session.revision. - Every
ConversationRowEntrynow carries row-levelturn_id. - Message rows (
user,assistant,thinking,system) may carryis_streamingandimages. - The server now upgrades many wrapper-style provider messages into semantic rows before they reach clients. For example, bootstrap prompts and environment blocks become
contextrows, lifecycle notices becomenoticerows, shell wrappers becomeshell_commandrows, and background task wrappers becometaskrows. - Passive Codex provider events are also materialized into typed timeline rows instead of being left as raw text. That includes
worker,plan,hook,handoff,approval, andquestionrows when the provider emits structured events.
Error responses:
404 not_found500 db_error503 runtime_error
Returns a paged slice of older conversation rows for infinite scroll.
Query params:
before_sequenceoptionallimitoptional, clamped to1...200, default50
Response:
{
"rows": [
{
"session_id": "od-...",
"sequence": 21,
"turn_id": "turn-3",
"row": {
"row_type": "tool",
"id": "tool-use-abc",
"provider": "claude",
"family": "search",
"kind": "grep",
"status": "completed",
"title": "Grep",
"invocation": { "...": "..." },
"result": { "...": "..." },
"render_hints": { "can_expand": true }
}
}
],
"total_row_count": 120,
"has_more_before": true,
"oldest_sequence": 21,
"newest_sequence": 70
}Common row families returned by both conversation paging endpoints:
- Message rows:
user,assistant,thinking,system - Semantic info rows:
context,notice,task - Command and execution rows:
shell_command,tool - Workflow rows:
worker,plan,hook,handoff,approval,question,activity_group
For exact payload fields, use docs/conversation-contracts.md as the source of truth.
Error responses:
404 not_found500 db_error503 runtime_error
Marks the session as read and persists the latest read position.
Response:
{
"session_id": "od-...",
"unread_count": 0
}Error responses:
404 session_not_found
Searches conversation rows within a session.
Query params:
qoptional substring match against row content/titlefamilyoptional tool-family filter such asshell,search,file_changestatusoptional tool-status filter such asrunning,completed,failedkindoptional tool-kind filter such asbash,grep,edit
Response:
{
"rows": [
{
"session_id": "od-...",
"sequence": 42,
"turn_id": "turn-7",
"row": {
"row_type": "tool",
"id": "tool-1",
"provider": "codex",
"family": "shell",
"kind": "bash",
"status": "completed",
"title": "Deploy preview build",
"duration_ms": 1200,
"invocation": { "...": "..." },
"result": { "...": "..." },
"render_hints": {}
}
}
],
"total_row_count": 1,
"has_more_before": false,
"oldest_sequence": 42,
"newest_sequence": 42
}Error responses:
404 not_found500 db_error503 runtime_error
Returns aggregate session metrics for dashboard and detail views.
Response:
{
"session_id": "od-...",
"total_rows": 120,
"tool_count": 45,
"tool_count_by_family": {
"shell": 12,
"file_change": 8
},
"failed_tool_count": 3,
"average_tool_duration_ms": 1200,
"turn_count": 8,
"total_tokens": {
"input_tokens": 50000,
"output_tokens": 12000,
"cached_tokens": 30000,
"context_window": 200000
},
"worker_count": 2,
"duration_ms": 300000
}Error responses:
404 not_found500 db_error503 runtime_error
Returns expanded content for a single conversation row (tool input/output, diffs, etc.).
Response:
{
"row_id": "tool-use-abc",
"input_display": "grep -r 'TODO' src/",
"output_display": "src/main.rs:42: // TODO: fix this",
"diff_display": [
{ "type": "add", "line": "+ new code", "line_number": 42 }
],
"language": "rust",
"start_line": 40
}All response fields except row_id are optional.
Error responses:
404 not_found500 db_error
Creates a direct session and returns the new summary immediately.
Request:
{
"provider": "codex",
"cwd": "/Users/.../repo",
"model": "gpt-5",
"approval_policy": "on-request",
"sandbox_mode": "workspace-write",
"permission_mode": "default",
"allowed_tools": [],
"disallowed_tools": [],
"effort": "medium",
"collaboration_mode": "default",
"multi_agent": true,
"personality": "balanced",
"service_tier": "priority",
"developer_instructions": "Stay concise",
"system_prompt": null,
"append_system_prompt": null
}All fields except provider and cwd are optional.
Response:
{
"session_id": "od-...",
"session": {
"id": "od-..."
}
}Notes:
- The server persists first, then tries to launch the connector.
session_createdis broadcast to list subscribers over WebSocket.
Resumes a persisted session.
Response:
{
"session_id": "od-...",
"session": {
"id": "od-..."
}
}Error responses:
404 session_not_found409 already_active422 missing_claude_resume_id500 db_error
Takes over a passive session.
Request:
{
"model": "gpt-5",
"approval_policy": "on-request",
"sandbox_mode": "workspace-write",
"permission_mode": "default",
"allowed_tools": [],
"disallowed_tools": []
}Response:
{
"session_id": "od-...",
"accepted": true
}Error responses:
404 not_found409 not_passive500 take_handle_failed500 connector_failed
Ends the session.
Response:
{"accepted": true}Sets or clears a custom session name.
Request:
{
"name": "Investigate approval drift"
}Request to clear:
{
"name": null
}Response:
{"accepted": true}Error responses:
404 not_found
Updates stored session config.
Request:
{
"approval_policy": "on-request",
"sandbox_mode": "workspace-write",
"permission_mode": "default",
"collaboration_mode": "default",
"multi_agent": true,
"personality": "balanced",
"service_tier": "priority",
"developer_instructions": "Stay concise",
"model": "gpt-5",
"effort": "high"
}All fields are optional.
Response:
{"accepted": true}Error responses:
404 not_found
Forks a session.
Request:
{
"nth_user_message": 3,
"model": "gpt-5",
"approval_policy": "on-request",
"sandbox_mode": "workspace-write",
"cwd": "/Users/.../repo",
"permission_mode": "default",
"allowed_tools": [],
"disallowed_tools": []
}All fields are optional.
Response:
{
"source_session_id": "od-source",
"new_session_id": "od-fork",
"session": {
"id": "od-fork"
}
}Error responses:
404 session_not_found422 not_foundwhen the source Codex connector is not active500 fork_failed500 channel_closed
Creates a worktree, then forks into it.
Request:
{
"branch_name": "feature/api-doc-pass",
"base_branch": "main",
"nth_user_message": 3
}Response:
{
"source_session_id": "od-source",
"new_session_id": "od-fork",
"session": {
"id": "od-fork"
},
"worktree": {
"id": "wt-..."
}
}Error responses:
404 session_not_found400 worktree_create_invalid_input500 worktree_create_failed- Plus the same fork errors as
POST /fork
Forks into an existing tracked worktree.
Request:
{
"worktree_id": "wt-...",
"nth_user_message": 3
}Response:
{
"source_session_id": "od-source",
"new_session_id": "od-fork",
"session": {
"id": "od-fork"
}
}Error responses:
400 worktree_repo_mismatch404 worktree_not_found410 worktree_missing- Plus the same fork errors as
POST /fork
Queues a new user turn.
Request:
{
"content": "Review the approval flow",
"model": "gpt-5",
"effort": "medium",
"skills": [],
"images": [],
"mentions": []
}At least one of content, images, mentions, or skills is required.
Response:
{
"accepted": true,
"row": {
"session_id": "od-...",
"sequence": 42,
"turn_id": "turn-8",
"row": {
"row_type": "user",
"id": "msg-...",
"content": "Review the approval flow",
"turn_id": "turn-8",
"timestamp": "2026-03-16T12:00:00Z",
"is_streaming": false,
"images": []
}
}
}Notes:
- Returns
202 Accepted. - The
rowfield contains the dispatched user message as aConversationRowEntry.
Error responses:
400 invalid_request- Session/dispatch errors from the active connector
Steers the active turn without creating a normal user turn.
Request:
{
"content": "Focus on REST-only routes",
"images": [],
"mentions": []
}At least one of content, images, or mentions is required.
Response:
{"accepted": true}Error responses:
400 invalid_request- Session/dispatch errors from the active connector
Interrupts the active turn.
Response:
{"accepted": true}Requests context compaction.
Response:
{"accepted": true}Undoes the last turn.
Response:
{"accepted": true}Rolls back the last n turns.
Request:
{
"num_turns": 2
}Response:
{"accepted": true}Error responses:
400 invalid_argumentwhennum_turns < 1
Stops a running task by task id.
Request:
{
"task_id": "task-..."
}Response:
{"accepted": true}Rewinds files to the state associated with a user message.
Request:
{
"user_message_id": "msg-..."
}Response:
{"accepted": true}Query params:
session_idoptionallimitoptional
Response:
{
"session_id": "od-...",
"approvals": []
}Error responses:
500 approval_list_failed
Response:
{
"approval_id": 42,
"deleted": true
}Error responses:
404 not_found500 approval_delete_failed
Approves or denies a tool request.
Request:
{
"request_id": "req-...",
"decision": "approved",
"message": "Looks good",
"interrupt": false,
"updated_input": {
"path": "src/main.rs"
}
}Only request_id and decision are required.
Response:
{
"session_id": "od-...",
"request_id": "req-...",
"outcome": "approved",
"active_request_id": null,
"approval_version": 9
}Error responses:
404 not_found400 invalid_answer_payload422 rollback_failed500for other dispatch failures
Answers a question approval.
Request:
{
"request_id": "req-...",
"answer": "Use the REST route",
"question_id": "question-1",
"answers": {
"selection": ["rest"]
}
}answer, question_id, and answers are optional individually, but the overall payload must still be meaningful to the active connector.
Response:
{
"session_id": "od-...",
"request_id": "req-...",
"outcome": "answered",
"active_request_id": null,
"approval_version": 10
}Error responses:
404 not_found400 invalid_answer_payload422 rollback_failed500for other dispatch failures
Responds to a permission grant request from the agent.
Request:
{
"request_id": "req-...",
"permissions": {
"Bash(git status:*)": "allow"
},
"scope": "project"
}permissions and scope are optional.
Response:
{
"session_id": "od-...",
"request_id": "req-...",
"outcome": "approved",
"active_request_id": null,
"approval_version": 11
}Error responses:
404 not_found400 invalid_answer_payload422 rollback_failed500for other dispatch failures
POST /api/sessions/{session_id}/attachments/images?display_name=<name>&pixel_width=<w>&pixel_height=<h>
Uploads an image attachment.
Request:
- Raw image bytes in the request body
Content-Typeheader is required and should be the image MIME type
Query params:
display_nameoptionalpixel_widthoptionalpixel_heightoptional
Response:
{
"image": {
"input_type": "attachment",
"value": "attachment-...",
"mime_type": "image/png"
}
}Error responses:
404 not_found400 invalid_requestif bytes or content type are missing500 attachment_store_failed
Returns the raw image bytes.
Response:
- Binary body
Content-Type: <stored mime type>
Error responses:
404 attachment_read_failed500 attachment_read_failed
Starts a shell command in the context of the session.
Request:
{
"command": "git status",
"cwd": "/Users/.../repo",
"timeout_secs": 120
}cwd and timeout_secs are optional. If cwd is omitted, the server uses the session's current cwd, then falls back to the project path.
Response:
{
"request_id": "shell-...",
"accepted": true
}Notes:
- Streaming shell output is delivered through WebSocket events and message updates.
Error responses:
404 session_not_found409 shell_duplicate_request_id
Cancels an active shell request.
Request:
{
"request_id": "shell-..."
}Response:
{"accepted": true}Error responses:
404 session_not_found404 shell_request_not_found
Response:
{
"session_id": "od-...",
"subagent_id": "subagent-...",
"tools": []
}Notes:
- If the subagent transcript is missing or unreadable, this returns an empty list.
Returns conversation rows for a subagent.
Response:
{
"session_id": "od-...",
"subagent_id": "subagent-...",
"rows": []
}Notes:
- Returns an empty list if the subagent transcript is unavailable.
Returns the instructions currently associated with the session.
Response:
{
"session_id": "od-...",
"provider": "codex",
"instructions": {
"developer_instructions": "Stay concise"
}
}Response for Claude may also include claude_md when either ~/.claude/CLAUDE.md or
<project>/CLAUDE.md exists:
{
"session_id": "od-...",
"provider": "claude",
"instructions": {
"claude_md": "# Project Instructions\n...",
"developer_instructions": "Stay concise"
}
}Notes:
system_promptis part of the response shape but is currentlynull/omitted.- For Claude,
claude_mdis the concatenated contents of global and projectCLAUDE.mdfiles when present.
Error responses:
404 not_found
Returns skills grouped by cwd.
Query params:
cwdoptional and repeatableforce_reloadoptional, defaultfalse
Response:
{
"session_id": "od-...",
"skills": [],
"errors": []
}Error responses:
409 session_not_found- Connector-specific MCP/skills startup errors surfaced through the dispatched action
Returns plugin marketplaces available to the session.
Query params:
cwdoptional and repeatable; used to discover repo-local marketplacesforce_remote_syncoptional, defaultfalse; refreshes curated remote plugin state first
Response:
{
"marketplaces": [
{
"name": "Curated",
"path": "/repo/.codex/plugins/marketplace.toml",
"interface": {
"displayName": "Curated Plugins"
},
"plugins": [
{
"id": "marketplace/deploy-checks",
"name": "deploy-checks",
"source": {
"type": "local",
"path": "/repo/.codex/plugins/deploy-checks"
},
"installed": true,
"enabled": true,
"installPolicy": "AVAILABLE",
"authPolicy": "ON_INSTALL",
"interface": null
}
]
}
],
"remoteSyncError": null
}Error responses:
409 session_not_found400 codex_action_error
Installs a plugin for the session.
Request:
{
"marketplacePath": "/repo/.codex/plugins/marketplace.toml",
"pluginName": "deploy-checks",
"forceRemoteSync": true
}Response:
{
"authPolicy": "ON_INSTALL",
"appsNeedingAuth": []
}Notes:
- OrbitDock clears plugin and skills caches after install so installed skills appear on the next refresh.
Uninstalls a plugin for the session.
Request:
{
"pluginId": "marketplace/deploy-checks",
"forceRemoteSync": true
}Response:
{}Returns the current MCP tool catalog.
Response:
{
"session_id": "od-...",
"tools": {},
"resources": {},
"resource_templates": {},
"auth_statuses": {}
}Error responses:
409 session_not_found
Notes:
- Codex sessions dispatch
ListMcpToolsfirst. - If that is unavailable, the server falls back to the Claude MCP route.
Refreshes MCP servers.
Request:
{
"server_name": "github"
}Request body is optional. Without it, the server refreshes the overall MCP state.
Response:
{"accepted": true}Notes:
- Returns
202 Accepted.
Enables or disables a Claude MCP server.
Request:
{
"server_name": "github",
"enabled": true
}Response:
{"accepted": true}Notes:
- Returns
202 Accepted.
Starts auth for a Claude MCP server.
Request:
{
"server_name": "github"
}Response:
{"accepted": true}Notes:
- Returns
202 Accepted.
Clears saved auth for a Claude MCP server.
Request:
{
"server_name": "github"
}Response:
{"accepted": true}Notes:
- Returns
202 Accepted.
Applies Claude MCP server config.
Request:
{
"servers": {
"github": {
"enabled": true
}
}
}Response:
{"accepted": true}Notes:
- Returns
202 Accepted.
Applies Claude flag settings.
Request:
{
"settings": {
"enablePlanner": true
}
}Response:
{"accepted": true}Notes:
- Returns
202 Accepted.
Returns the effective permission rules for the active session.
Response for Claude:
{
"session_id": "od-...",
"rules": {
"provider": "claude",
"rules": []
}
}Response for Codex:
{
"session_id": "od-...",
"rules": {
"provider": "codex",
"approval_policy": "on-request",
"sandbox_mode": "workspace-write"
}
}Error responses:
404 not_found
Notes:
- Claude first tries
get_settingsfrom the running CLI, then falls back to on-disk settings.
Adds a Claude permission rule.
Request:
{
"pattern": "Bash(git status:*)",
"behavior": "allow",
"scope": "project"
}scope defaults to project. Use global to write to ~/.claude/settings.local.json.
Response:
{"ok": true}Error responses:
404 not_found500 serialize_error500 write_error
Removes a Claude permission rule.
Request body matches the add route.
Response:
{"ok": true}Error responses:
404 not_found500 serialize_error500 write_error
Query params:
turn_idoptional
Response:
{
"session_id": "od-...",
"review_revision": 5,
"comments": []
}Notes:
- If loading fails, this returns an empty list.
Creates a review comment.
Request:
{
"turn_id": "turn-...",
"file_path": "src/main.rs",
"line_start": 42,
"line_end": 45,
"body": "This needs error handling",
"tag": "risk"
}turn_id, line_end, and tag are optional.
Response:
{
"session_id": "od-...",
"review_revision": 6,
"comment_id": "rc-...",
"deleted": false,
"ok": true
}Notes:
- Server broadcasts
review_comment_createdto session subscribers over WebSocket.
Updates a review comment.
Request:
{
"body": "Updated text",
"tag": "nit",
"status": "resolved"
}All fields are optional.
Response:
{
"session_id": "od-...",
"review_revision": 7,
"comment_id": "rc-...",
"deleted": false,
"ok": true
}Error responses:
404 not_found500 review_comment_update_failed
Deletes a review comment.
Response:
{
"session_id": "od-...",
"review_revision": 8,
"comment_id": "rc-...",
"deleted": true,
"ok": true
}Error responses:
404 not_found500 review_comment_delete_failed
Response:
{"configured": true}Stores the OpenAI API key.
Request:
{
"key": "sk-..."
}Response:
{"configured": true}Sets whether this server is primary.
Request:
{
"is_primary": true
}Response:
{"is_primary": true}Notes:
- Broadcasts a
server_infoupdate to connected WebSocket clients.
Registers or clears a client's primary claim.
Request:
{
"client_id": "client-...",
"device_name": "Robert's MacBook Pro",
"is_primary": true
}Response:
{"accepted": true}Notes:
- Broadcasts a
server_infoupdate to connected WebSocket clients.
Response:
{
"usage": null,
"error_info": {
"code": "not_control_plane_endpoint",
"message": "This endpoint is not primary for control-plane usage reads."
}
}Same response shape as Codex usage.
Response:
{
"models": [
{
"id": "gpt-5",
"model": "gpt-5",
"display_name": "GPT-5",
"description": "General-purpose coding model",
"is_default": true,
"supported_reasoning_efforts": ["low", "medium", "high"],
"supports_reasoning_summaries": true
}
]
}Response:
{
"models": [
{
"value": "claude-sonnet-4-5",
"display_name": "Claude Sonnet 4.5",
"description": "Balanced speed and quality"
}
]
}Returns current Codex auth/account state.
Query params:
refresh_tokenoptional, defaultfalse
Response:
{
"status": {
"auth_mode": "chatgpt",
"requires_openai_auth": true,
"account": {
"type": "chatgpt",
"email": "user@example.com",
"plan_type": "plus"
},
"login_in_progress": false
}
}Error responses:
503 codex_auth_error
Starts the ChatGPT browser login flow.
Response:
{
"login_id": "...",
"auth_url": "https://..."
}Error responses:
500 codex_auth_login_start_failed
Notes:
- If account state is available, the server broadcasts it over WebSocket right after starting login.
Cancels an in-progress login.
Request:
{
"login_id": "..."
}Response:
{
"login_id": "...",
"status": "canceled"
}Status values:
cancelednot_foundinvalid_id
Notes:
- The server broadcasts refreshed account status when available.
Logs out the current Codex account.
Response:
{
"status": { }
}Error responses:
500 codex_auth_logout_failed
Notes:
- Broadcasts
codex_account_updatedto connected WebSocket clients.
Inspects the effective Codex configuration for a given working directory. Resolves settings from user config, project config, and OrbitDock overrides.
Request:
{
"cwd": "/Users/.../repo",
"codex_config_source": "user",
"model": "o3",
"approval_policy": "on-request",
"sandbox_mode": "workspace-write"
}Only cwd is required. Other fields provide overrides for what-if inspection.
Response:
{
"effective_settings": { },
"origins": { },
"layers": [ ],
"warnings": [ ]
}Returns available config profiles and providers for a given working directory.
Response:
{
"cwd": "/Users/.../repo",
"effective_settings": { },
"profiles": [ ],
"providers": [ ],
"warnings": [ ]
}Returns the raw Codex config documents (user-level and project-level) for inspection.
Response:
{
"cwd": "/Users/.../repo",
"user": { },
"projects": [ ],
"warnings": [ ]
}Writes a single config value to a Codex config file.
Request:
{
"cwd": "/Users/.../repo",
"key_path": "model",
"value": "o3",
"merge_strategy": "replace",
"file_path": null,
"expected_version": null
}merge_strategy is optional, one of "replace" or "upsert". expected_version enables optimistic concurrency.
Response:
{
"status": "written",
"version": "v2",
"file_path": "/Users/.../.codex/config.json"
}Writes multiple config values atomically.
Request:
{
"cwd": "/Users/.../repo",
"edits": [
{ "key_path": "model", "value": "o3", "merge_strategy": "replace" },
{ "key_path": "approval_policy", "value": "on-request" }
],
"file_path": null,
"expected_version": null
}Response: same shape as POST /api/codex/config/value.
Returns the default Codex config source preference.
Response:
{
"default_config_source": "user"
}default_config_source is one of "user" or "orbitdock".
Updates the default Codex config source preference.
Request:
{
"default_config_source": "orbitdock"
}Response: same shape as GET /api/server/codex-preferences.
Runs git init in the target directory.
Request:
{
"path": "/Users/.../new-project"
}Response:
{"ok": true}Error responses:
400 path_not_found400 git_init_failed
Lists directory entries.
Query params:
pathoptional, defaults to the user's home directory
Response:
{
"path": "/Users/.../repo",
"entries": [
{
"name": "src",
"is_dir": true,
"is_git": false
}
]
}Notes:
- Hidden entries are omitted.
- Results are sorted with directories first, then case-insensitive name.
~is expanded to the current home directory.- Read failures return an empty
entrieslist instead of an error.
Returns recently active project roots.
Response:
{
"projects": [
{
"path": "/Users/.../repo",
"session_count": 3,
"last_active": "1735689600Z"
}
]
}Returns tracked or discovered worktrees for a repo root.
Query params:
repo_rootoptional
Response:
{
"repo_root": "/path/to/repo",
"worktree_revision": 12,
"worktrees": []
}Notes:
- Without
repo_root, this currently returns an empty list. - If the database has no tracked rows for the repo, the server falls back to
git worktree listdiscovery.
Creates a tracked worktree.
Request:
{
"repo_path": "/path/to/repo",
"branch_name": "feature-x",
"base_branch": "main"
}Response:
{
"repo_root": "/path/to/repo",
"worktree_revision": 13,
"worktree": {
"id": "wt-...",
"repo_root": "/path/to/repo",
"worktree_path": "/path/to/repo/.orbitdock-worktrees/feature-x",
"branch": "feature-x",
"status": "active"
}
}Error responses:
400 create_failed
Notes:
- Broadcasts
worktree_createdto list subscribers over WebSocket. - If
repo_path/.worktreeincludeexists, OrbitDock tries to copy matching local ignored files into the new worktree.
Discovers worktrees for a repo path without requiring tracked DB rows.
Request:
{
"repo_path": "/path/to/repo"
}Response:
{
"repo_root": "/path/to/repo",
"worktree_revision": 12,
"worktrees": []
}DELETE /api/worktrees/{worktree_id}?force=true|false&delete_branch=true|false&delete_remote_branch=true|false&archive_only=true|false
Removes or archives a tracked worktree.
Query params:
forceoptional, defaultfalsedelete_branchoptional, defaultfalsedelete_remote_branchoptional, defaultfalsearchive_onlyoptional, defaultfalse
Response:
{
"repo_root": "/path/to/repo",
"worktree_revision": 14,
"worktree_id": "wt-...",
"deleted": true,
"ok": true
}Error responses:
404 not_found400 remove_failed
Notes:
force=truekeeps going even ifgit worktree removefails.archive_only=trueskips on-disk deletion and only updates tracked state.- Broadcasts
worktree_removedto list subscribers over WebSocket.
Returns all missions.
Response:
{
"missions": [
{
"id": "mission-...",
"name": "API improvements",
"repo_root": "/Users/.../repo",
"enabled": true,
"paused": false,
"tracker_kind": "linear",
"provider": "claude",
"provider_strategy": "single",
"primary_provider": "claude",
"secondary_provider": null,
"active_count": 2,
"queued_count": 5,
"completed_count": 12,
"failed_count": 1,
"parse_error": null,
"orchestrator_status": "polling"
}
]
}Creates a new mission.
Request:
{
"name": "API improvements",
"repo_root": "/Users/.../repo",
"tracker_kind": "linear",
"provider": "claude"
}Only name and repo_root are required. tracker_kind defaults to "linear", provider defaults to "claude".
Response: a single MissionSummary (same shape as the list items above).
Returns full mission detail including issues, settings, and file status.
Response:
{
"summary": {
"id": "mission-...",
"name": "API improvements",
"repo_root": "/Users/.../repo",
"enabled": true,
"paused": false,
"tracker_kind": "linear",
"provider": "claude",
"provider_strategy": "single",
"primary_provider": "claude",
"secondary_provider": null,
"active_count": 2,
"queued_count": 5,
"completed_count": 12,
"failed_count": 1,
"parse_error": null,
"orchestrator_status": "polling"
},
"issues": [
{
"issue_id": "issue-...",
"identifier": "ENG-42",
"title": "Fix auth flow",
"tracker_state": "In Progress",
"orchestration_state": "running",
"session_id": "od-...",
"provider": "claude",
"attempt": 1,
"error": null,
"url": "https://linear.app/team/issue/ENG-42",
"last_activity": "2026-03-16T12:00:00Z",
"started_at": "2026-03-16T11:55:00Z",
"completed_at": null,
"allowed_transitions": ["queued", "completed", "blocked", "failed"],
"work_status": "working",
"last_message": "Implementing auth changes...",
"pr_url": "https://github.com/owner/repo/pull/97"
}
],
"settings": {
"provider": {
"strategy": "single",
"primary": "claude",
"secondary": null,
"max_concurrent": 3,
"max_concurrent_primary": null
},
"agent": {
"claude": {
"model": "claude-sonnet-4-5",
"effort": "high",
"permission_mode": "default",
"allowed_tools": [],
"disallowed_tools": []
},
"codex": {
"model": "gpt-5",
"effort": "medium",
"approval_policy": "on-request",
"sandbox_mode": "workspace-write",
"collaboration_mode": null,
"multi_agent": null,
"personality": null,
"service_tier": null,
"developer_instructions": null
}
},
"trigger": {
"kind": "polling",
"interval": 30,
"filters": {
"labels": [],
"states": [],
"project": "ENG",
"team": null
}
},
"orchestration": {
"max_retries": 3,
"stall_timeout": 600,
"base_branch": "main",
"worktree_root_dir": null,
"state_on_dispatch": "In Progress",
"state_on_complete": "In Review"
},
"prompt_template": "You are working on {{ issue.identifier }}...",
"tracker": "linear"
},
"mission_file_exists": true,
"mission_file_path": "/Users/.../repo/MISSION.md",
"workflow_migration_available": false
}Notes:
settingsisnullwhen the mission file cannot be parsed.orchestration_stateis one of:queued,claimed,running,retry_queued,completed,failed,blocked.work_statusandlast_messageare populated from the live session snapshot when the linked agent is active.pr_urlis set when the agent links a pull request viamission_link_pr.allowed_transitionslists the valid target states for admin transition from the current state.
Updates mission metadata.
Request:
{
"name": "Updated name",
"enabled": true,
"paused": false,
"mission_file_path": "/Users/.../repo/MISSION.md"
}All fields are optional. Set mission_file_path to null to clear a custom path.
Response:
{"ok": true}Deletes a mission and returns the updated list.
Response:
{
"missions": []
}Returns the issue list for a mission.
Response: array of MissionIssueItem (same shape as issues in the detail response).
Retries a failed issue. The issue must be in failed state.
Response:
{"ok": true}Notes:
- Increments the attempt counter.
- Schedules the next retry with exponential backoff (max 300s).
Transitions an issue to a new orchestration state. Used for admin state overrides (mark complete, mark failed, reset, etc.).
Request:
{
"target_state": "completed",
"reason": "Manually closed — already fixed upstream"
}reason is optional. target_state must be one of the allowed transitions from the issue's current state. See OrchestrationState.allowed_transitions().
Response: MissionDetailResponse (same shape as GET /api/missions/{mission_id}).
Reports that the agent working on this issue has completed successfully. Called by the mission orchestrator or agent tools.
Request:
{
"tracker_state": "In Review"
}tracker_state is optional — when provided, updates the issue's tracker state label in the database.
Response:
{"completed": true}Stores a PR URL on a mission issue. Called by the MCP mission tools when an agent links a PR via mission_link_pr.
Request:
{
"pr_url": "https://github.com/owner/repo/pull/97"
}Response:
{"ok": true}Notes:
- Broadcasts a mission delta so connected clients see the PR link immediately.
- The PR URL is surfaced in the issue row UI alongside the issue identifier.
Reports that the agent working on this issue is blocked. Called by mission tools (mission_report_blocked).
Request body:
{"reason": "Missing LINEAR_API_KEY — cannot interact with tracker"}Response:
{"blocked": true}Notes:
- Updates
orchestration_stateto"blocked"with the reason inlast_error. - The mission orchestrator will not retry blocked issues automatically.
Writes a default MISSION.md template to the mission's repo_root.
Response: MissionDetailResponse (same shape as GET /api/missions/{mission_id}).
Error responses:
409 conflictifMISSION.mdalready exists
Migrates an existing WORKFLOW.md (Symphony format) to MISSION.md.
Response: MissionDetailResponse (same shape as GET /api/missions/{mission_id}).
Error responses:
404 not_foundifWORKFLOW.mddoes not exist409 conflictifMISSION.mdalready exists
Returns the default prompt template for a mission.
Response:
{
"template": "You are working on {{ issue.identifier }}..."
}Updates mission settings. Performs a partial merge with existing MISSION.md config.
Request:
{
"provider_strategy": "single",
"primary_provider": "claude",
"secondary_provider": null,
"max_concurrent": 3,
"max_concurrent_primary": null,
"agent_claude_model": "claude-sonnet-4-5",
"agent_claude_effort": "high",
"agent_claude_permission_mode": "default",
"agent_claude_allowed_tools": [],
"agent_claude_disallowed_tools": [],
"agent_codex_model": "gpt-5",
"agent_codex_effort": "medium",
"agent_codex_approval_policy": "on-request",
"agent_codex_sandbox_mode": "workspace-write",
"agent_codex_collaboration_mode": null,
"agent_codex_multi_agent": null,
"agent_codex_personality": null,
"agent_codex_service_tier": null,
"agent_codex_developer_instructions": null,
"trigger_kind": "polling",
"poll_interval": 30,
"label_filter": [],
"state_filter": [],
"project_key": "ENG",
"team_key": null,
"max_retries": 3,
"stall_timeout": 600,
"base_branch": "main",
"worktree_root_dir": null,
"prompt_template": "You are working on {{ issue.identifier }}...",
"tracker": "linear"
}All fields are optional. Only provided fields are merged.
Response: MissionDetailResponse (same shape as GET /api/missions/{mission_id}).
Starts the polling orchestrator for a mission.
Response:
{"ok": true}Error responses:
400 bad_requestif tracker API key is not configured409 conflictif orchestrator is already running
Manually dispatch a specific tracker issue to a mission. Fetches the issue from Linear by identifier, upserts it into the mission's issue list, and spawns a dispatch (worktree + session).
Request:
{
"issue_identifier": "VIZ-240",
"provider": "claude"
}provider is optional — defaults to the mission's primary provider.
Response: MissionDetailResponse (same shape as GET /api/missions/{id}).
Error responses:
400 bad_requestif tracker API key is not configured or MISSION.md cannot be parsed404 not_foundif mission or issue not found
Triggers an immediate poll cycle for the mission's orchestrator. Useful when you know new issues are available and don't want to wait for the next scheduled tick.
Response:
{"ok": true}Returns all worktrees associated with a mission's issues (via the mission_issues -> sessions -> worktrees join). Used by the "Clean Up Worktrees" UI.
Response:
{
"worktrees": [
{
"id": "wt-...",
"branch": "mission/eng-42",
"worktree_path": "/Users/.../repo/.orbitdock-worktrees/mission/eng-42",
"disk_present": true,
"orchestration_state": "completed",
"issue_identifier": "ENG-42",
"issue_title": "Fix auth flow"
}
]
}Notes:
- Only returns worktrees with status !=
"removed". disk_presentis checked live against the filesystem.- A single issue may map to multiple worktrees if it was retried.
Dispatched sessions automatically receive 8 mission_* tools for tracker interaction (mission_get_issue, mission_post_update, mission_update_comment, mission_get_comments, mission_set_status, mission_link_pr, mission_create_followup, mission_report_blocked).
Tool injection is provider-dependent:
- Claude sessions: A
.mcp.jsonfile is auto-generated in the worktree root, configuring anorbitdock-missionMCP server via theorbitdock mcp-mission-toolssubcommand. Claude discovers this at startup. - Codex sessions: Tools are registered as
DynamicToolSpecentries and passed to the thread at creation time.
The blocked endpoint above (POST .../blocked) is called by the mission_report_blocked tool executor.
Response:
{"configured": true}Stores the Linear API key.
Request:
{
"key": "lin_api_..."
}Response:
{"configured": true}Removes the stored Linear API key.
Response:
{"configured": false}Response:
{"configured": true}Stores the GitHub personal access token.
Request:
{
"key": "ghp_..."
}Response:
{"configured": true}Removes the stored GitHub API key.
Response:
{"configured": false}Returns the configuration status of all tracker API keys.
Response:
{
"linear": {
"configured": true,
"source": "settings"
},
"github": {
"configured": true,
"source": "env"
}
}Notes:
sourceindicates where the key was found:"env"(environment variable) or"settings"(persisted in server settings).
Returns the default provider strategy for new missions.
Response:
{
"provider_strategy": "single",
"primary_provider": "claude",
"secondary_provider": null
}Updates the default provider strategy.
Request:
{
"provider_strategy": "round_robin",
"primary_provider": "claude",
"secondary_provider": "codex"
}All fields are optional.
Response: same shape as GET /api/server/mission-defaults.
WebSocket is used for:
- dashboard, missions, and session-surface subscriptions
- real-time turn interaction
- server-pushed updates
- approval prompts and results
- shell streaming updates
- worktree, review comment, and auth status broadcasts
The server sends a hello immediately after connect:
{
"type": "hello",
"hello": {
"server_version": "0.4.0",
"compatibility": {
"compatible": true,
"server_compatibility": "server_authoritative_session_v1"
},
"capabilities": [
"dashboard_projection_v1",
"missions_projection_v1",
"session_detail_surface_v1",
"session_composer_surface_v1",
"conversation_surface_v1"
]
}
}The handshake is informational. OrbitDock should surface real transport or decode failures directly instead of trying to negotiate protocol-version compatibility at runtime.
WebSocket handshake request headers should include:
Authorization: Bearer <token>when auth is enabledX-OrbitDock-Client-Version: <client-version>on current clients
Common client messages include:
subscribe_dashboardsubscribe_missionssubscribe_session_surfaceunsubscribe_session_surfacecreate_sessionresume_sessionsend_messageapprove_toolanswer_questioninterrupt_session
subscribe_dashboard, subscribe_missions, and subscribe_session_surface all support:
since_revisionoptional
Example:
{
"type": "subscribe_session_surface",
"session_id": "od-...",
"surface": "conversation",
"since_revision": 120
}WebSocket does not bootstrap heavy surface state. Use HTTP first, then subscribe with since_revision.
Server-pushed event types:
hello— compatibility handshake + capabilitiesdashboard_invalidated/missions_invalidated— list refresh hintsconversation_rows_changed— incremental row upserts/removalssession_delta— session metadata changes (status, tokens, name, etc.)approval_requested— tool needs user approvalapproval_decision_result— approval outcometokens_updated— token usage snapshotsession_ended/session_forkedshell_started/shell_output— shell execution streamingcontext_compacted/undo_started/undo_completed/thread_rolled_backrate_limit_event/prompt_suggestion/files_persistedskills_list/mcp_tools_list/mcp_startup_update/mcp_startup_completereview_comment_created/review_comment_updated/review_comment_deletedworktree_created/worktree_removed/worktree_status_changed
See docs/conversation-contracts.md for the typed row schema used in conversation_rows_changed.
Notes:
conversation_rows_changeduses the same typed row families as the REST conversation endpoints.- Wrapper-style provider text is normalized on the server before broadcast. Clients should treat row typing as authoritative and should not need to parse raw XML-like wrappers such as
<environment_context>,<turn_aborted>, or<user_shell_command>.