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.

Operation Input Context HTTP mount Purpose
agent.list {} request GET /api/agent Available agents.
auth.activate { accountID: AccountID } server POST /api/auth/:accountID/activate Set 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 } server POST /api/auth Create an auth account.
auth.delete { accountID: AccountID } server DELETE /api/auth/:accountID Remove an auth account.
auth.get { accountID: AccountID } server GET /api/auth/:accountID Get one auth account.
auth.list { serviceID?: ServiceID } server GET /api/auth List 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> } } server PATCH /api/auth/:accountID Update account description or credential.
catalog.model.get { providerID: ProviderID modelID: ModelID } server GET /api/catalog/model/:providerID/:modelID Get one catalog model.
catalog.model.list {} server GET /api/catalog/model List flattened catalog models.
command.list {} request GET /api/command Available commands.
config.get {} request GET /api/config Resolved config.
config.update { config: Config } request PATCH /api/config Update config.
event.subscribe {} request GET /api/event Server-sent events for the resolved runtime context.
formatter.status {} request GET /api/formatter Formatter status.
fs.file { path: string } request GET /api/fs/file Read one file.
fs.grep { pattern: string include?: string limit?: number } request POST /api/fs/grep Search file contents.
fs.search { query: string type?: "file" | "directory" limit?: number } request POST /api/fs/search Search paths by name.
fs.tree { path: string } request GET /api/fs/tree Browse a directory.
lsp.status {} request GET /api/lsp LSP status.
mcp.prompt.list {} request GET /api/mcp/prompt List MCP prompts.
mcp.prompt.render { server: string name: string arguments?: Record<string, string> } request POST /api/mcp/prompt/render Render one MCP prompt.
mcp.resource.list {} request GET /api/mcp/resource List MCP resources.
mcp.resource.read { server: string uri: string } request GET /api/mcp/resource/read Read 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 } } request POST /api/mcp/server Add an MCP server to runtime config.
mcp.server.list {} request GET /api/mcp/server List MCP servers with status and auth state.
mcp.server.oauth.callback { name: string code: string } request POST /api/mcp/server/:name/oauth/callback Complete MCP OAuth.
mcp.server.oauth.delete { name: string } request DELETE /api/mcp/server/:name/oauth Remove MCP OAuth credentials.
mcp.server.oauth.start { name: string } request POST /api/mcp/server/:name/oauth Start MCP OAuth.
permission.list {} request GET /api/permission Pending permission requests.
permission.reply { permissionID: PermissionID response: PermissionReply } request POST /api/permission/:permissionID/reply Reply to a permission request.
project.get { projectID: ProjectID } server GET /api/project/:projectID Get project metadata.
project.list {} server GET /api/project List projects known to this server.
project.update { projectID: ProjectID name?: string icon?: string commands?: Array<{ name: string command: string }> } server PATCH /api/project/:projectID Update project metadata.
provider.list {} request GET /api/provider Provider inventory for the runtime context.
pty.create { command?: string cwd?: string shell?: string } request POST /api/pty Create PTY in the runtime context.
pty.delete { ptyID: PtyID } request DELETE /api/pty/:ptyID Delete PTY.
pty.get { ptyID: PtyID } request GET /api/pty/:ptyID Get PTY info.
pty.list {} request GET /api/pty List PTYs for the runtime.
pty.update { ptyID: PtyID title?: string size?: { columns: number, rows: number } } request PATCH /api/pty/:ptyID Update PTY.
question.list {} request GET /api/question Pending user questions.
question.reject { questionID: QuestionID } request POST /api/question/:questionID/reject Reject a question.
question.reply { questionID: QuestionID response: QuestionResponse } request POST /api/question/:questionID/reply Reply to a question.
session.compact { sessionID: SessionID } session POST /api/session/:sessionID/compact Compact the session conversation.
session.context { sessionID: SessionID } session GET /api/session/:sessionID/context Return active context messages after the last compaction.
session.create { title?: string agent?: string model?: { providerID: ProviderID, modelID: ModelID } permission?: PermissionRule[] } request POST /api/session Create a session pinned to resolved runtime context.
session.delete { sessionID: SessionID } session DELETE /api/session/:sessionID Delete a session.
session.diff { sessionID: SessionID } session GET /api/session/:sessionID/diff Return session diff summary.
session.get { sessionID: SessionID } session GET /api/session/:sessionID Get one session.
session.list { limit?: number order?: "asc" | "desc" path?: string roots?: boolean start?: number search?: string cursor?: string } request GET /api/session List sessions for the current runtime context by default.
session.message.list { sessionID: SessionID limit?: number order?: "asc" | "desc" cursor?: string } session GET /api/session/:sessionID/message Page through session messages.
session.prompt { sessionID: SessionID prompt: Prompt delivery?: "immediate" | "deferred" } session POST /api/session/:sessionID/prompt Create a user message and queue the agent loop.
session.todo { sessionID: SessionID } session GET /api/session/:sessionID/todo Return todos associated with the session.
session.update { sessionID: SessionID title?: string archived?: number permission?: PermissionRule[] } session PATCH /api/session/:sessionID Update title, archival state, or session metadata.
session.wait { sessionID: SessionID } session POST /api/session/:sessionID/wait Wait until the session is idle.
skill.list {} request GET /api/skill Available skills.
vcs.diff { format?: "json" | "patch" mode?: "worktree" | "default" } request GET /api/vcs/diff Diff for the runtime directory.
vcs.get {} request GET /api/vcs VCS metadata.
vcs.patch { patch: string } request POST /api/vcs/patch Apply a patch to the runtime directory.
vcs.status {} request GET /api/vcs/status Changed files.
workspace.create { projectID?: ProjectID name?: string directory?: string type: string metadata?: Record<string, unknown> } server POST /api/workspace Create or register a workspace.
workspace.delete { workspaceID: WorkspaceID } server DELETE /api/workspace/:workspaceID Remove a workspace registration.
workspace.get { workspaceID: WorkspaceID } server GET /api/workspace/:workspaceID Get workspace metadata.
workspace.list { projectID?: ProjectID } server GET /api/workspace List workspaces, optionally filtered by project.
workspace.status {} server GET /api/workspace/status Connection/lifecycle status for all workspaces. Needs team discussion.
workspace.sync {} server POST /api/workspace/sync Sync workspace metadata from adapters. Needs team discussion.
workspace.update { workspaceID: WorkspaceID name?: string metadata?: Record<string, unknown> archived?: boolean } server PATCH /api/workspace/:workspaceID Update workspace metadata or lifecycle state.
workspace.warp { workspaceID?: WorkspaceID sessionID: SessionID copyChanges: boolean } server POST /api/workspace/warp Move 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}`
}