Terminal API

Local HTTP interfaces in app 1.22.0: terminals, agent sessions, layouts, keep-alive, notifications, service tokens, and webhooks.

Setup and security

Enable Settings > Remote Control > Terminal API and generate a token. The server binds to 127.0.0.1, using port 3037 in production and 3036 in development. The general API is disabled by default. Keep it on loopback; it is not a public remote-access server.

Authorization: Bearer $TERMINAL_API_TOKEN
Content-Type: application/json

JSON success responses use { success: true, data: ... }; failures use { success: false, error: { code, message, reason?, details? } }. Branch on error.code. The general limit is 120 requests per 60 seconds per IP/method/path; exceeding it returns 429 RATE_LIMITED. Bootstrap challenge and webhook ingress are authentication exceptions described below.

Workspaces and sessions

GET /api/terminal/workspaces

Returns data.workspaces with id, name, and rootPath.

{ "success": true, "data": { "workspaces": [
  { "id": "ws_123", "name": "my-project", "rootPath": "/path/to/project" }
] } }

POST /api/terminal/sessions

Required: workspaceId. Optional: name, cols (80–500, default 80), rows (24–100, default 24), logPolicy: "persist", background (default false), paneId, stealFocus. Returns data.session with id, applied name, workspaceId, status, isActive, and background. Creation starts at the workspace root.

For agent automation, set background: true to avoid taking a tab or pane. Foreground sessions can target paneId and use stealFocus: false; placement options cannot be combined with background: true (INVALID_PLACEMENT).

GET /api/terminal/sessions

Optional query workspaceId filters sessions. Returns data.sessions. Terminal sessions have no idle TTL; they end on deletion, PTY exit, shutdown, or worker crash cleanup.

PATCH /api/terminal/sessions/:id

Body: { name }. Returns data.sessionId and data.name. Collisions add -2, -3, etc.; use the returned name. Names are limited to 200 characters.

Session path targets accept an id or unique tab name; URL-encode names. An id wins over a matching name. Unresolved or ambiguous names return SESSION_NOT_FOUND; resolution failure returns SESSION_RESOLVE_FAILED. Resolve to an id for automation that must remain stable across renames. SSE resolves once when subscribing.

GET /api/terminal/sessions/:id/status

Includes activity, readiness, hasPrompt, waitingForInput, and prompt.kind (shell, confirm, select, password, open). Check readiness === "idle" before the next command; activity alone is insufficient.

GET /api/terminal/sessions/:id/log

For sessions created with logPolicy: "persist". Returns sessionId, logPath, content under data. Persisted files can outlive the session.

GET /api/terminal/sessions/:id/ports

Returns data.sessionId and data.ports: [{ pid, port, protocol, address }].

DELETE /api/terminal/sessions/:id

Destroy the terminal session and release its resources.

Input

POST /api/terminal/sessions/:id/input

Required JSON string data, or a text/plain body. Options in JSON or query: appendEnter, submitKey, queueUntilReady, readyTimeoutMs, agentSessionId. submitKey overrides appendEnter and follows the /keys names. Text and the submit key are written separately.

{ "data": "npm test", "appendEnter": true, "queueUntilReady": true, "readyTimeoutMs": 30000 }

queueUntilReady waits for readiness "idle". A readiness timeout returns TERMINAL_NOT_READY without writing. Invalid data returns 400 INVALID_INPUT_DATA; an invalid submitKey writes nothing. Input handling returns data.requestId on success or error.details.requestId on failure, except requests rejected before input handling.

POST /api/terminal/sessions/:id/submit

JSON { data, settleMs?, queueUntilReady?, readyTimeoutMs?, agentSessionId? }. Use when already at an interactive prompt, rather than after every shell command.

POST /api/terminal/sessions/:id/keys

JSON { key }. Supported keys: enter, up, down, left, right, tab, esc, ctrl+c, ctrl+d.

POST /api/terminal/sessions/:id/interrupt

Send Ctrl+C without destroying the session.

A session held by a remote peer rejects /input, /submit, /keys, and /interrupt with 409 SESSION_HELD_BY_PEER. Take it over on this host before writing. Do not retry unchanged refusals.

Output and SSE

GET /api/terminal/sessions/:id/output

Query: mode, lines, since, chromeProfile, ifHash, source, waitForChange, timeoutMs. text strips ANSI while preserving visible text; raw keeps ANSI; content filters TUI chrome on a best-effort basis; screen reads the current screen, not history.

Responses include data.sessionId, mode, lines, cursor, nextCursor, hasMore, truncated, windowStart, windowEnd, and lineCount. Reuse nextCursor as since for incremental reads. screen has no since support; it prefers headless state, with DOM fallback requiring a visible pane. source=headless or source=dom forces that path. For content, chromeProfile accepts claude-code, default, or none.

Screen responses include outputHash and unchanged. Send ifHash on the next screen read; a matching hash returns unchanged: true with empty lines. waitForChange=true enables long polling; data.wait includes changed, timedOut, and optional reason: "LONGPOLL_TIMEOUT".

GET /api/terminal/sessions/:id/events

SSE frames contain TerminalSessionOutputEvent JSON: { type: "output.new", sessionId, timestamp, payload }. payload has the /output shape. Query: mode=raw|text|content, since, lines (clamped to 1–500), chromeProfile, timeoutMs (at least 1000 ms). screen returns 400 INVALID_TERMINAL_SSE_QUERY. Without since the stream starts at the current tail.

Terminal SSE does not replay a retained backlog. Recover missed history with /output?since=... before subscribing, then advance using payload.nextCursor. Slow readers can be disconnected.

Layout

GET /api/terminal/layout

Returns data.layout. Use ?full=true for contentType/contentId bindings, pane geometry, and activePanelId; the default slim format carries id and terminalId.

POST /api/terminal/layout

JSON { type, sessionIds? }, for example { "type": "horizontal-2", "sessionIds": ["build", "test"] }.

POST /api/terminal/layout/panes/:paneId/assign

JSON { sessionId }; null clears a pane. Returns data.paneId and resolved data.terminalId.

POST /api/terminal/layout/panes/:paneId/activate

Focus a pane.

POST /api/terminal/layout/panels/:panelId/activate

Activate a tab-layer panel, including one without a pane binding.

POST /api/terminal/layout/restore

Body is the data.layout snapshot from ?full=true. Unavailable content is reported in restored.skipped; unresolved targets appear in restored.unresolvedTargets when present. A slim snapshot leaves panes empty and reports SLIM_PANE.

curl -s -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:3037/api/terminal/layout?full=true" | jq .data.layout > layout.json
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d @layout.json \
  http://127.0.0.1:3037/api/terminal/layout/restore

Keep-alive and notifications

GET /api/terminal/sessions/:id/keepalive

List rules.

PUT /api/terminal/sessions/:id/keepalive

JSON { rule: { id, enabled, schedule, message, promptRef? } }. Creates or replaces rule.id. Schedule: interval/intervalMs, idle/thresholdMs, daily/time (HH:mm), once/atMs. Idle rules require a local session.

DELETE /api/terminal/sessions/:id/keepalive/:ruleId

Remove a rule. All three keep-alive operations return the resulting rule list, including nextFireAt (null when disabled).

{ "rule": { "id": "review", "enabled": true,
  "schedule": { "kind": "interval", "intervalMs": 1800000 },
  "message": "continue" } }

At most 10 enabled rules per session. To read a live Prompt Library entry at each trigger, set message: "" and promptRef: { slug: "review", scope: "project" } (or scope: "global"). References and inline text are mutually exclusive; prompts must have no variables. Project references use the target’s local workspace. Invalid prompts skip the trigger. Rules are removed when the session is destroyed.

POST /api/terminal/notify

JSON { message, sessionId?, title? }. Requires Remote Control, push notifications, and a connected provider. Message: 1–4000 trimmed characters; title: 1–100, only for the first Discord thread creation. Limit: 6 messages per minute per session.

When sessionId is present and Discord supports session threads, the push targets Discord. Otherwise it uses connected remote providers. Without a session it goes to connected remotes. Success means at least one provider accepted delivery; an undelivered push returns a non-2xx response.

Agent sessions

This surface manages provider sessions and structured transcripts, separate from raw terminal sessions. Providers: claude, codex, agy, duo, opencode; gemini is accepted as an alias for agy and new responses use agy.

POST /api/agent-sessions

JSON { provider, cwd, prompt, env?, settingSources? }. Returns data.session; its identifier is sessionId. env is provider-filtered; interactive Claude does not apply per-request env. Set credentials in the shell environment before app launch. settingSources is stored but not forwarded to the CLI.

GET /api/agent-sessions

Optional query cwd. Returns data.sessions.

POST /api/agent-sessions/resolve

JSON { cwd, shellPid? }. Returns data.target for attaching to an existing provider session.

POST /api/agent-sessions/attach

JSON { provider, sessionId, cwd }. Returns data.session. Attach accepts a Termdock run sessionId or the provider conversation id (metadata.aiSessionId). Use the returned run sessionId for later operations.

GET /api/agent-sessions/:id

Returns data.status with session (including session.lifetime), dispatch, bufferedEventCount, lastEventSequence, hasRenderedView, renderedEntryCount, lastRenderedSequence, and canRestart.

POST /api/agent-sessions/:id/input

JSON { input, env? }. Returns data.sessionId after provider send. A dispatch timeout can leave delivery uncertain; reconcile before resending.

POST /api/agent-sessions/:id/interrupt

Soft-interrupt the current turn when the provider supports it; returns data.sessionId.

POST /api/agent-sessions/:id/restart

Restart using cached create input. Returns data.session; unavailable cached input yields AGENT_SESSION_RESTART_UNAVAILABLE.

DELETE /api/agent-sessions/:id

Stop the agent and return data.sessionId. Retained events can remain until session.lifetime.evictsAt.

GET /api/agent-sessions/:id/events

SSE lifecycle, assistant, tool, result, and error events. Query since is an exclusive sequence cursor. The newest 200 events are retained; detect gaps and resync from /rendered.

GET /api/agent-sessions/:id/rendered

Returns data.view, a rendered transcript snapshot, not a terminal screenshot.

GET /api/agent-sessions/:id/rendered/stream

SSE transcript snapshots and updates. Treat bounded snapshots as replacements, not append-only history.

POST /api/agent-sessions/hooks

Ingest provider hooks for Workstream. Prefer CLI hooks setup and hook ingest for provider-specific payloads and replies.

POST /api/agent-sessions/:id/callbacks

Required source, eventKind, message; optional dedupeKey and JSON-object metadata. Limits: source/eventKind 256 characters, dedupeKey 512, message 32768. Unknown fields are rejected. Returns data { sessionId, deliveryId, createdAt }.

Callbacks target an existing AgentSession id or its terminal id/unique name. They never create or attach an agent and do not answer permission prompts. Keys do not deduplicate automatically; metadata is caller-supplied, not authenticated identity. Success acknowledges provider send, not task completion.

Service tokens

UI tokens have full access. Service tokens scope access to terminal and/or agent-session; omitting scopes grants both. Bootstrap provisioning works while the general API is disabled, but terminal and agent endpoints still return 403 DISABLED.

POST /api/auth/bootstrap-challenge

No bearer token. JSON { nonce } (32–256 characters). Returns data.proof: hex HMAC-SHA256(bootstrapSecret, "<nonce>|<port>"). Verify locally with a constant-time comparison before sending the secret; a missing or mismatched proof must stop provisioning.

POST /api/auth/service-tokens

JSON { name, scopes? }. Authenticate using the UI token or X-Termdock-Bootstrap-Token from ~/.termdock/config/terminal-api-bootstrap.json. Returns data.serviceToken including the token once; store it securely.

POST /api/auth/service-tokens/:id/rotate

JSON { graceSeconds? }. A service token can rotate only itself; the UI token can administer tokens. Old token expiry is previousTokenExpiresAt.

DELETE /api/auth/service-tokens/:id

Revoke with the UI token or the service token itself.

Webhook ingress

POST /api/webhooks/:id/:secret

Create the URL in Settings > Automation > Webhooks for a local workspace and tab name. It requires no bearer token and works while the general API is disabled. JSON must contain exactly { message, submit }. message is one line of 1–4096 UTF-16 characters with no ASCII controls. submit is a boolean. Body limit: 8 KiB; 10 requests per 60 seconds per webhook.

The URL is shown once and is a secret. The target must be an agent waiting at its input composer. Busy, missing, ambiguous, non-agent, or unreadable-screen targets fail without writing or creating a session. submit: true also sends Enter. Success is { success: true, data: { delivered: true } }.

For external senders, configure a TLS-enabled tunnel or reverse proxy exposing only /api/webhooks/... and keep secrets out of access logs. Never expose the other Terminal API routes. Revoke URLs in Settings.

Automation setup

Example: background terminal

BASE="http://127.0.0.1:3037"
AUTH="Authorization: Bearer $TERMINAL_API_TOKEN"
WS=$(curl -s -H "$AUTH" "$BASE/api/terminal/workspaces" | jq -r ".data.workspaces[0].id")
SID=$(curl -s -X POST -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"workspaceId\":\"$WS\",\"background\":true}" \
  "$BASE/api/terminal/sessions" | jq -r ".data.session.id")
curl -s -X POST -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"data":"pwd","appendEnter":true,"queueUntilReady":true}' \
  "$BASE/api/terminal/sessions/$SID/input"
curl -s -H "$AUTH" "$BASE/api/terminal/sessions/$SID/output?mode=text&lines=20"
curl -s -X DELETE -H "$AUTH" "$BASE/api/terminal/sessions/$SID"
CLI command reference