opencode v2

API map

A single /api route surface for simple clients and multi-directory frontends. The important design question is not route nesting; it is where runtime context comes from.

Server scoped Request context Session pinned
Everything has one canonical route. Some routes are server-scoped; runtime routes use context; session item routes use the session.

Server-scoped routes manage the whole server: projects, workspace lifecycle, and auth accounts. Runtime context is for anything resolved from an active directory, including config, provider capabilities, tools, files, and VCS.

Context Model

API context resolution Non-session routes resolve from request context, session item routes resolve from session storage. Non-session route /api/file, /api/vcs/status Request context query params or default runtime Runtime context directory + workspaceID? Session item route /api/session/:id/prompt Session row contains pinned context Runtime context directory + workspaceID?

Request-context calls

These calls operate against a directory, optionally through a workspace. Simple clients omit context and use the default runtime.

GET /api/fs/tree?path=.&directory=/repo/app&workspace=ws_123

Session-pinned calls

These calls never take request context. The session is already pinned to the directory and workspace it was created in.

POST /api/session/ses_123/prompt

// server resolves
sessionID -> { directory, workspaceID? }

Operation Inventory

The SDK is the source of truth. HTTP routes are mounts for RPC-style operations. server operations do not use runtime context. request operations use request/default runtime context from directory and workspace query parameters. session operations use pinned session context and should not accept context input.

OperationInputContextHTTP mountPurpose
agent.list{}requestGET /api/agentAvailable agents.
auth.activate{ accountID: AccountID }serverPOST /api/auth/:accountID/activateSet the account as active for its service.
auth.create{ serviceID: ServiceID credential: | { type: "oauth", refresh: string, access: string, expires: number } | { type: "api", key: string, metadata?: Record<string, string> } description?: string active?: boolean }serverPOST /api/authCreate an auth account.
auth.delete{ accountID: AccountID }serverDELETE /api/auth/:accountIDRemove an auth account.
auth.get{ accountID: AccountID }serverGET /api/auth/:accountIDGet one auth account.
auth.list{ serviceID?: ServiceID }serverGET /api/authList saved auth accounts. Response includes active account mapping.
auth.update{ accountID: AccountID description?: string credential?: | { type: "oauth", refresh: string, access: string, expires: number } | { type: "api", key: string, metadata?: Record<string, string> } }serverPATCH /api/auth/:accountIDUpdate account description or credential.
catalog.model.get{ providerID: ProviderID modelID: ModelID }serverGET /api/catalog/model/:providerID/:modelIDGet one catalog model.
catalog.model.list{}serverGET /api/catalog/modelList flattened catalog models.
command.list{}requestGET /api/commandAvailable commands.
config.get{}requestGET /api/configResolved config.
config.update{ config: Config }requestPATCH /api/configUpdate config.
event.subscribe{}requestGET /api/eventServer-sent events for the resolved runtime context.
formatter.status{}requestGET /api/formatterFormatter status.
fs.file{ path: string }requestGET /api/fs/fileRead one file.
fs.grep{ pattern: string include?: string limit?: number }requestPOST /api/fs/grepSearch file contents.
fs.search{ query: string type?: "file" | "directory" limit?: number }requestPOST /api/fs/searchSearch paths by name.
fs.tree{ path: string }requestGET /api/fs/treeBrowse a directory.
lsp.status{}requestGET /api/lspLSP status.
mcp.prompt.list{}requestGET /api/mcp/promptList MCP prompts.
mcp.prompt.render{ server: string name: string arguments?: Record<string, string> }requestPOST /api/mcp/prompt/renderRender one MCP prompt.
mcp.resource.list{}requestGET /api/mcp/resourceList MCP resources.
mcp.resource.read{ server: string uri: string }requestGET /api/mcp/resource/readRead one MCP resource.
mcp.server.create{ name: string config: | { type: "local", command: string, arguments?: string[], environment?: Record<string, string> } | { type: "remote", url: string, headers?: Record<string, string>, oauth?: boolean | object } }requestPOST /api/mcp/serverAdd an MCP server to runtime config.
mcp.server.list{}requestGET /api/mcp/serverList MCP servers with status and auth state.
mcp.server.oauth.callback{ name: string code: string }requestPOST /api/mcp/server/:name/oauth/callbackComplete MCP OAuth.
mcp.server.oauth.delete{ name: string }requestDELETE /api/mcp/server/:name/oauthRemove MCP OAuth credentials.
mcp.server.oauth.start{ name: string }requestPOST /api/mcp/server/:name/oauthStart MCP OAuth.
permission.list{}requestGET /api/permissionPending permission requests.
permission.reply{ permissionID: PermissionID response: PermissionReply }requestPOST /api/permission/:permissionID/replyReply to a permission request.
project.get{ projectID: ProjectID }serverGET /api/project/:projectIDGet project metadata.
project.list{}serverGET /api/projectList projects known to this server.
project.update{ projectID: ProjectID name?: string icon?: string commands?: Array<{ name: string command: string }> }serverPATCH /api/project/:projectIDUpdate project metadata.
provider.list{}requestGET /api/providerProvider inventory for the runtime context.
pty.create{ command?: string cwd?: string shell?: string }requestPOST /api/ptyCreate PTY in the runtime context.
pty.delete{ ptyID: PtyID }requestDELETE /api/pty/:ptyIDDelete PTY.
pty.get{ ptyID: PtyID }requestGET /api/pty/:ptyIDGet PTY info.
pty.list{}requestGET /api/ptyList PTYs for the runtime.
pty.update{ ptyID: PtyID title?: string size?: { columns: number, rows: number } }requestPATCH /api/pty/:ptyIDUpdate PTY.
question.list{}requestGET /api/questionPending user questions.
question.reject{ questionID: QuestionID }requestPOST /api/question/:questionID/rejectReject a question.
question.reply{ questionID: QuestionID response: QuestionResponse }requestPOST /api/question/:questionID/replyReply to a question.
session.compact{ sessionID: SessionID }sessionPOST /api/session/:sessionID/compactCompact the session conversation.
session.context{ sessionID: SessionID }sessionGET /api/session/:sessionID/contextReturn active context messages after the last compaction.
session.create{ title?: string agent?: string model?: { providerID: ProviderID, modelID: ModelID } permission?: PermissionRule[] }requestPOST /api/sessionCreate a session pinned to resolved runtime context.
session.delete{ sessionID: SessionID }sessionDELETE /api/session/:sessionIDDelete a session.
session.diff{ sessionID: SessionID }sessionGET /api/session/:sessionID/diffReturn session diff summary.
session.get{ sessionID: SessionID }sessionGET /api/session/:sessionIDGet one session.
session.list{ limit?: number order?: "asc" | "desc" path?: string roots?: boolean start?: number search?: string cursor?: string }requestGET /api/sessionList sessions for the current runtime context by default.
session.message.list{ sessionID: SessionID limit?: number order?: "asc" | "desc" cursor?: string }sessionGET /api/session/:sessionID/messagePage through session messages.
session.prompt{ sessionID: SessionID prompt: Prompt delivery?: "immediate" | "deferred" }sessionPOST /api/session/:sessionID/promptCreate a user message and queue the agent loop.
session.todo{ sessionID: SessionID }sessionGET /api/session/:sessionID/todoReturn todos associated with the session.
session.update{ sessionID: SessionID title?: string archived?: number permission?: PermissionRule[] }sessionPATCH /api/session/:sessionIDUpdate title, archival state, or session metadata.
session.wait{ sessionID: SessionID }sessionPOST /api/session/:sessionID/waitWait until the session is idle.
skill.list{}requestGET /api/skillAvailable skills.
vcs.diff{ format?: "json" | "patch" mode?: "worktree" | "default" }requestGET /api/vcs/diffDiff for the runtime directory.
vcs.get{}requestGET /api/vcsVCS metadata.
vcs.patch{ patch: string }requestPOST /api/vcs/patchApply a patch to the runtime directory.
vcs.status{}requestGET /api/vcs/statusChanged files.
workspace.create{ projectID?: ProjectID name?: string directory?: string type: string metadata?: Record<string, unknown> }serverPOST /api/workspaceCreate or register a workspace.
workspace.delete{ workspaceID: WorkspaceID }serverDELETE /api/workspace/:workspaceIDRemove a workspace registration.
workspace.get{ workspaceID: WorkspaceID }serverGET /api/workspace/:workspaceIDGet workspace metadata.
workspace.list{ projectID?: ProjectID }serverGET /api/workspaceList workspaces, optionally filtered by project.
workspace.status{}serverGET /api/workspace/statusConnection/lifecycle status for all workspaces. Needs team discussion.
workspace.sync{}serverPOST /api/workspace/syncSync workspace metadata from adapters. Needs team discussion.
workspace.update{ workspaceID: WorkspaceID name?: string metadata?: Record<string, unknown> archived?: boolean }serverPATCH /api/workspace/:workspaceIDUpdate workspace metadata or lifecycle state.
workspace.warp{ workspaceID?: WorkspaceID sessionID: SessionID copyChanges: boolean }serverPOST /api/workspace/warpMove a session into or out of a workspace. Needs team discussion.

Event Envelope

Every event uses the same envelope. Resource identity belongs in payload. Runtime identity belongs in context.

type ApiEvent<Payload> = {
  id: string
  type: string
  time: number
  context: {
    directory: string
    workspaceID?: string
  }
  payload: Payload
}
{
  "id": "evt_01",
  "type": "message.part.delta",
  "time": 1760000000000,
  "context": {
    "directory": "/repo/app",
    "workspaceID": "ws_123"
  },
  "payload": {
    "sessionID": "ses_123",
    "messageID": "msg_456",
    "partID": "part_789",
    "field": "text",
    "delta": "hello"
  }
}

Frontend Sync Store

A frontend can keep one giant store like the current TUI. Runtime data is partitioned by contextKey. Durable entities such as sessions and messages are keyed by their own IDs.

type RuntimeContext = {
  directory: string
  workspaceID?: string
}

type ContextKey = string
type SessionID = string
type MessageID = string

type SyncStore = {
  status: "loading" | "partial" | "complete"

  shared: {
    provider: Provider[]
    provider_default: Record<string, string>
    provider_next: ProviderListResponse
    provider_auth: Record<string, ProviderAuthMethod[]>
    console_state: ConsoleState
  }

  contexts: Record<
    ContextKey,
    {
      context: RuntimeContext

      config: Config
      agent: Agent[]
      command: Command[]
      lsp: LspStatus[]
      formatter: FormatterStatus[]
      vcs: VcsInfo | undefined
      mcp: Record<string, McpStatus>
      mcp_resource: Record<string, McpResource>

      session: SessionID[]
      session_status: Record<SessionID, SessionStatus>
    }
  >

  session: Record<SessionID, Session & { context: RuntimeContext }>
  session_diff: Record<SessionID, Snapshot.FileDiff[]>
  todo: Record<SessionID, Todo[]>
  permission: Record<SessionID, PermissionRequest[]>
  question: Record<SessionID, QuestionRequest[]>

  message: Record<SessionID, Message[]>
  part: Record<MessageID, Part[]>
}

function contextKey(context: RuntimeContext) {
  return `${context.workspaceID ?? "local"}:${context.directory}`
}