Desk’s browser UI and CLI talk to a local HTTP server. The stock server has no built-in authentication, so the API is a local-trust surface. Default base URL:

Response shape

JSON routes return JSON with an appropriate status code. Errors are usually:
Unexpected failures are collapsed to a terse request failed response. Stack fields are redacted from JSON payloads.

Core routes

/api/desk returns the manifest-backed workspace snapshot. /api/system returns the cached system metrics snapshot. /api/pulse is the UI’s regular heartbeat. It includes system metrics, running session ids, attention state, unread event count, LSP status, and channel runtime state. /api/up { dryRun? } starts every missing manifest session and, unless it is a dry run, boots every explorer terminal whose shell is missing and whose folder still exists. It answers { exitCode, error?, actions, explorerTerminals? }; explorerTerminals is { booted, skipped, errors } (ids; skipped are the terminals whose folder is gone, errors are boot <id>: <reason> and make the call fail with exitCode 1) and is absent on a dry run. /api/explorer-terminals manages the shells opened from the editor’s file tree. Every route answers { terminals: [...] }, each entry { sessionId, cwd, name, command?, createdAt, state } with state running or missing. POST /api/explorer-terminals { cwd, name?, command? } needs an existing absolute directory (~ expands). name (1-64 characters) is the display name and the source of the term-... id; it defaults to the folder name. command (up to 4096 characters) runs in that directory instead of the login shell and is re-run on boot; blank means the login shell. The entry is committed under the manifest lock, the shell is provisioned through the terminal daemon, and the manifest is rolled back if provisioning fails; the response also carries created. /boot { sessionId } re-provisions a known entry; /delete { sessionId } retires the shell first and keeps the entry if the daemon refuses. /delete-under { cwd } takes an absolute folder (~ expands; it need not exist any more) and retires every terminal whose folder is that folder or lies below it, all at once, then removes the entries whose retire succeeded in one manifest write. It answers 200 with { terminals, removed, errors }: removed lists the ids it forgot, errors lists retire <id>: <reason> for the entries it kept; a relative cwd is a 400 invalid-input. /edit { sessionId, name, cwd, command? } rewrites the entry under the same validation as create (the sessionId never changes; a blank or absent command clears it; the name must be unique in the target folder among the other terminals, else 409). When the shell is running and the folder or the command changed, the edit retires it with reason operator-reboot and provisions it again with the new values; a rename alone never touches the daemon. If that respawn fails the route answers 500 but keeps the edited entry, so the list shows it missing for a later boot. /restart { sessionId } retires a known entry with reason operator-reboot and provisions it again; it refuses with 400 when the folder no longer exists and answers 500 with the daemon’s error when the retire or the provision is refused. A malformed body is invalid-input (400) and an unknown id is not-found (404).

Session and layout mutation routes

Desk uses API routes for UI mutations such as:
  • add, edit, delete, move, reorder, and restart sessions
  • add, edit, delete, move, and reorder groups
  • add, edit, delete, move, and reorder projects
  • update group layout and persisted split sizes
The server validates cwd values, layout kinds, custom cell counts, and split-size percentages before writing the manifest.

Attention and agent events

Use /api/attention-read to mark events as read. Send { "clear": true } to the same route to clear the attention-event list. Agent hooks and plugins post typed events to /api/agent-event. Desk uses those events for attention, resume capture, and channel-engine release signals.

Terminal routes

Terminal capture accepts a durable sessionId, row count, and offset. It proxies the terminal daemon’s bounded emulator history and returns color-preserving lines for the frozen scrollback viewer. Live resize, repaint, input, and subscription traffic use the binary WebSocket rather than REST routes.

Terminal WebSockets

/ws/terminal is the only browser terminal path. One binary connection per browser tab carries multiple terminal surfaces. Hidden surfaces retain their channel under VISIBILITY false; the daemon rejects their input, ignores their resize authority, and sends no live output deltas. Same-channel reveal normally receives one bounded contiguous OUTPUT catch-up from its retained cursor. A cursor gap or terminal revision change receives a current-screen SNAPSHOT instead. Only actual surface removal or transport loss unsubscribes. Frames are channel- and generation-fenced through the supervised terminal daemon to the session’s Moor holder.

Filesystem routes

Filesystem routes live under /api/fs. They include:
  • home/root discovery
  • notes root and notes state
  • path validation
  • directory listing
  • file read and raw media read
  • download: one file as an attachment, or several files / a folder streamed as a .zip (GET /api/fs/download; GET /api/fs/download-plan is the validating pre-flight the explorer calls first, answering the archive name, byte total, entry count and how many unreadable entries were skipped). Archives are uncompressed (STORE) with per-entry data descriptors and keep empty folders; readers that scan strictly front-to-back without the central directory (Java’s ZipInputStream, funzip) cannot open them, everything that reads the central directory can. Selections beyond 65,535 members or 4 GiB are refused up front (no ZIP64).
  • filename and content search
  • write, create, rename, copy, and delete operations
  • LSP-aware preview/apply flows for create, rename, and delete
The server constrains paths to the active root, with a special allowance for the manifest file. Writes are atomic and mtime-guarded so external edits can produce a conflict instead of being silently overwritten.

Git routes

Git routes live under /api/git. They cover:
  • repository discovery
  • status and status maps
  • log/history
  • line diffs
  • worktree, index, commit, range, and branch diffs
  • stage and unstage
  • discard
  • commit and amend
  • fetch, pull, push, and publish
  • branch checkout, create, and delete
  • worktree removal
  • commit revert
  • GitHub repository and PR context
  • open-on-GitHub URL resolution
Desk runs the host git and gh commands and reports failures through JSON errors.

GitHub Projects routes

GitHub Projects routes live under /api/projects. They cover:
  • auth/scope checks
  • project listing
  • board data loading
  • item detail loading
  • owner listing
  • field value updates
  • item position changes
  • adding items by URL
  • draft item creation, edit, and conversion
  • archive, unarchive, and delete
  • status update create/delete
  • issue and pull request edits/comments/state changes
  • project create/edit/link-repo operations
The backend uses gh api graphql, gh issue, and gh pr.

Channels routes

Channels routes live under /api/channels. They cover:
  • channel state
  • engine diagnostics and actions
  • channel listing and message windows
  • threads
  • reactions
  • saved views
  • paused sessions
  • search
  • featured messages
  • uploads and file serving
  • activity events
  • export
  • message create, edit, and delete
  • member add and remove
  • share-to-channel
  • queue control
The CLI prefers these server routes when DESK_API is reachable, and falls back to direct markdown append for some post flows when the server is unavailable.

Usage routes

Both routes read stored state from the Channels process of the running Desk. They never contact a provider; that process checks profiles in the background. /api/usage/snapshot lists every configured profile and the ambient:claude and ambient:codex entries. Each entry has profileId, kind, provider, label, support (supported, unsupported with a reason, or unknown before the first check), an opaque quotaScope when the provider confirmed the account identity, and lastCheck. account and usage are independent observations: value, observedAt, staleAfter, attemptedAt, result (ok or a code such as rate-limited) and freshness (never, fresh or stale). An account value is available, unavailable with a reason (not-logged-in, expired, rejected, access-denied) or unknown. A usage value lists limits (id, scope, usedPercent, windowSeconds, resetsAt, allowed), credits in their provider unit, plan and partial. Unknown durations, reset times and balances are null, never zero. Two profiles with the same quotaScope share one quota. The snapshot also carries revision, configurationRevision, generatedAt and the collector state. /api/usage/history returns 10-minute UTC slices of one profile, oldest first, for the last 90 days by default. from and to are RFC 3339 times; limit is 1–1000 (default 200); nextCursor continues the page. Each slice has its interval, instance (a new one after the id is reused), series (unknown or the quota scope), check counts, authorizationFailures and the same observations as the snapshot, with freshness judged at the end of the interval. Errors keep their codes: invalid-input (400), unknown-profile (404), payload-too-large (413), usage-incompatible or unsupported-version (502), and uninitialized, service-unavailable or a storage code (503). A failed read is never reported as an empty snapshot.

LSP routes and sockets

The HTTP endpoint requires a bearer token for session-scoped agent access. It validates file URIs against the configured workspace root and redacts sensitive command, environment, token, and path data from responses. The browser uses LSP wiring for editor features and diagnostics. Managed agents use the MCP wrapper described in Agent integrations.

Plugins

Plugins can add middleware, routes, and WebSocket upgrade guards for local embedders and downstream builds. See Security and plugin model before adding runtime extensions.

Next steps