API Reference
TermBeam exposes a REST API and WebSocket interface.
Contents
Section titled “Contents”REST API
Section titled “REST API”All API endpoints (except /login, /api/auth, /api/auth/status, /api/auth/passkeys/login/options, /api/auth/passkeys/login, /api/version, and /api/config) require authentication via cookie or Bearer token.
Bearer auth failure (401):
{ "error": "unauthorized" }Returned when the Authorization: Bearer token does not match the server password, or when an API request has no valid authentication. Bearer auth is also rate-limited to 5 attempts per minute per IP (returns 429).
Authentication
Section titled “Authentication”POST /api/auth
Section titled “POST /api/auth”Sign in with the password or a one-time QR/share token and receive a session cookie.
Request:
{ "password": "your-password", "remember": true }or, for a one-time token from the QR code / share link:
{ "ott": "one-time-token", "remember": true }| Field | Type | Description |
|---|---|---|
password |
string | The access password |
ott |
string | A one-time share token (5-minute, single use) |
remember |
boolean | Default true: 7-day sliding / 30-day max session saved to disk. false: 2-hour / 12-hour, memory only |
Response (200):
{ "ok": true }Sets an httpOnly, SameSite=Strict cookie pty_token (with Max-Age when remembered, a browser-session cookie otherwise).
Response (401):
{ "error": "wrong password" }Response (429):
{ "error": "Too many attempts. Try again later." }GET /api/auth/status
Section titled “GET /api/auth/status”Public. Tells the login screen what is possible from this address. Pass the page origin so passkey availability can be checked against the host you are using.
GET /api/auth/status?origin=https%3A%2F%2Fabcd-3456.euw.devtunnels.ms
{ "passwordRequired": true, "authenticated": false, "passkeys": { "enabled": true, "registered": true }, "session": null}passkeys.enabled is true only for secure origins (HTTPS or http://localhost) that match the host TermBeam is reached at. session ({ method, createdAt }) is present when the cookie is valid.
POST /api/auth/logout
Section titled “POST /api/auth/logout”Signs the current device out (revokes its session) and clears the cookie.
GET /api/auth/sessions
Section titled “GET /api/auth/sessions”Lists signed-in devices.
{ "sessions": [ { "id": "Xk2…", "device": "iPhone · Safari", "method": "passkey", "remember": true, "ip": "203.0.113.7", "createdAt": 1760000000000, "lastSeenAt": 1760000500000, "expiresAt": 1760600000000, "current": true } ]}DELETE /api/auth/sessions/:id
Section titled “DELETE /api/auth/sessions/:id”Signs one device out. Its open WebSockets are closed immediately with code 4401.
POST /api/auth/sessions/revoke-others
Section titled “POST /api/auth/sessions/revoke-others”Signs out every device except the current one. Returns { "ok": true, "revoked": 3 }.
Passkeys (WebAuthn)
Section titled “Passkeys (WebAuthn)”Passkey endpoints are available on secure origins only (the HTTPS tunnel URL or http://localhost) and are rate-limited to 30 requests per minute per IP. Options and credentials use base64url JSON.
| Endpoint | Auth | Description |
|---|---|---|
POST /api/auth/passkeys/register/options |
Cookie | Body { origin } (the page’s location.origin). Returns { publicKey } creation options (challenge valid for 3 minutes, single use) |
POST /api/auth/passkeys/register |
Cookie | { credential, name? } — verifies and stores the passkey |
POST /api/auth/passkeys/login/options |
Public | Body { origin }. Returns { publicKey } request options, or 404 when no passkey exists for the host |
POST /api/auth/passkeys/login |
Public | { credential, remember? } — verifies the assertion and sets the session cookie |
GET /api/auth/passkeys |
Required | { passkeys: [{ id, name, rpId, createdAt, lastUsedAt, synced }] } |
PATCH /api/auth/passkeys/:id |
Required | { name } — rename |
DELETE /api/auth/passkeys/:id |
Required | Remove a passkey and sign out the devices that used it |
Sessions
Section titled “Sessions”GET /api/sessions
Section titled “GET /api/sessions”List all active sessions.
Response:
[ { "id": "a1b2c3d4", "name": "my-project", "cwd": "/home/user/project", "shell": "/bin/zsh", "pid": 12345, "clients": 1, "createdAt": "2025-01-01T00:00:00.000Z", "color": "#4a9eff", "lastActivity": 1719849600000, "home": "/home/user", "git": { "branch": "main", "repoName": "owner/repo", "provider": "GitHub", "root": "/Users/me/src/repo", "status": { "clean": false, "modified": 1, "staged": 0, "untracked": 2, "ahead": 3, "behind": 0, "summary": "1 modified, 2 untracked, 3↑" } } }]| Field | Type | Description |
|---|---|---|
color |
string | Hex color assigned to the session |
lastActivity |
number | Unix timestamp (ms) of the last PTY output |
cwd |
string | Live working directory of the shell process (updates when the user cds) |
home |
string | Home directory of the user running TermBeam — clients show paths under it (and only it) as ~ |
git |
object/null | Git repository info for the session’s cwd, or null if not in a git repo |
git.branch |
string | Current branch name (or short SHA in detached HEAD) |
git.root |
string/null | Repository top-level directory; sessions with the same root share a working tree (the hub groups them) |
git.repoName |
string/null | Remote repository name (e.g. owner/repo) |
git.provider |
string/null | Hosting provider: GitHub, GitLab, Bitbucket, Azure DevOps, or host |
git.status |
object | Working tree status with clean, modified, staged, untracked, ahead, behind, and summary fields |
POST /api/sessions
Section titled “POST /api/sessions”Create a new session.
Request:
{ "name": "My Session", "shell": "/bin/bash", "args": ["-l"], "cwd": "/home/user", "initialCommand": "htop", "color": "#4ade80", "cols": 120, "rows": 30}All fields are optional. If initialCommand is provided, it will be sent to the shell after startup (capped at 8192 characters — longer values are rejected with 400 initialCommand too long). If color is omitted, a color is assigned automatically from a built-in palette. The optional cols and rows fields set the initial terminal size (defaults to 120×30 if omitted).
The shell field is validated against the list of detected shells (see GET /api/shells). The cwd field must be an absolute path to an existing directory.
Copilot sessions: send "type": "copilot" to start a GitHub Copilot session (via the Copilot SDK) with a hidden companion terminal. The optional model is a Copilot model id from GET /api/copilot/models; omit it to let Copilot use the account’s current default. The response then also includes ptySessionId and model — the model in use, or null until Copilot resolves its default on the first reply. A malformed model returns 400 model must be a Copilot model id.
Response (201):
{ "id": "e5f6g7h8", "url": "/terminal?id=e5f6g7h8"}Response (400):
{ "error": "Invalid shell" }{ "error": "cwd must be an absolute path" }{ "error": "cwd is not a directory" }{ "error": "cwd does not exist" }PATCH /api/sessions/:id
Section titled “PATCH /api/sessions/:id”Update session properties.
Request:
{ "color": "#f87171", "name": "renamed-session"}All fields are optional.
Response (200):
{ "ok": true }Response (404):
{ "error": "not found" }GET /api/sessions/:id/detect-port
Section titled “GET /api/sessions/:id/detect-port”Scan a session’s scrollback buffer for the last localhost or 127.0.0.1 URL and return the port number.
Response (200) — port found:
{ "detected": true, "port": 3000 }Response (200) — no port found:
{ "detected": false }Response (404):
{ "error": "not found" }DELETE /api/sessions/:id
Section titled “DELETE /api/sessions/:id”Kill and remove a session.
Response (204): No content.
Response (404):
{ "error": "not found" }File Operations
Section titled “File Operations”GET /api/sessions/:id/files
Section titled “GET /api/sessions/:id/files”Browse files and directories within a session’s working directory.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
dir |
string | Subdirectory path relative to session CWD. Defaults to .. |
Response (200):
{ "base": "/home/user/project/src", "rootDir": "/home/user/project", "entries": [ { "name": "components", "type": "directory", "size": 0, "modified": "2024-01-15T10:30:00.000Z" }, { "name": "index.ts", "type": "file", "size": 1234, "modified": "2024-01-15T10:30:00.000Z" } ]}| Field | Type | Description |
|---|---|---|
base |
string | Absolute path to the listed directory |
rootDir |
string | Absolute path to the session’s working directory |
entries |
array | Files and directories in the listed path |
Each entry contains name (string), type ("file" or "directory"), size (number, bytes), and modified (string or null — ISO 8601 timestamp, or null if stat fails).
Entries are sorted directories-first, then files alphabetically. Hidden files (starting with .) and symbolic links are excluded.
Response (400):
{ "error": "Invalid dir parameter" }Response (401):
{ "error": "unauthorized" }Response (404):
{ "error": "Session not found" }Response (500):
{ "error": "Failed to read directory" }GET /api/sessions/:id/download
Section titled “GET /api/sessions/:id/download”Download a file from within a session’s working directory.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
file |
string | File path relative to session CWD (required). |
Response: Binary file content with Content-Disposition: attachment header.
Response (400):
{ "error": "Missing file parameter" }{ "error": "Not a regular file" }Response (401):
{ "error": "unauthorized" }Response (403):
{ "error": "Symlink target is outside session directory" }Response (404):
{ "error": "Session not found" }{ "error": "File not found" }Response (413):
{ "error": "File too large (max 100 MB)" }GET /api/sessions/:id/file-content
Section titled “GET /api/sessions/:id/file-content”Get the text content of a file. Used for in-browser file preview (e.g., markdown rendering).
Query parameters:
| Parameter | Type | Description |
|---|---|---|
file |
string | File path relative to session CWD (required). |
Response (200):
{ "content": "# Hello World\n\nThis is a markdown file.", "name": "README.md", "size": 42}| Field | Type | Description |
|---|---|---|
content |
string | UTF-8 text content of the file |
name |
string | Filename (basename) |
size |
number | File size in bytes |
Response (400):
{ "error": "Missing file parameter" }{ "error": "Not a regular file" }Response (401):
{ "error": "unauthorized" }Response (403):
{ "error": "Symlink target is outside session directory" }Response (404):
{ "error": "Session not found" }{ "error": "File not found" }Response (413):
{ "error": "File too large (max 2 MB)" }GET /api/sessions/:id/file-raw
Section titled “GET /api/sessions/:id/file-raw”Serve a file inline from a session’s working directory. Unlike the /download endpoint, this does not set a Content-Disposition: attachment header, making it suitable for in-browser rendering (e.g., images).
Query parameters:
| Parameter | Type | Description |
|---|---|---|
file |
string | File path relative to session CWD (required). |
Response: File content served inline with the appropriate Content-Type header.
Response (400):
{ "error": "Missing file parameter" }{ "error": "Not a regular file" }Response (401):
{ "error": "unauthorized" }Response (403):
{ "error": "Symlink target is outside session directory" }Response (404):
{ "error": "Session not found" }{ "error": "File not found" }Response (413):
{ "error": "File too large (max 20 MB)" }GET /api/shells
Section titled “GET /api/shells”List available shells on the host system.
Requires authentication (cookie or Bearer token).
Response:
{ "shells": [ { "name": "bash", "path": "/bin/bash", "cmd": "/bin/bash" }, { "name": "zsh", "path": "/bin/zsh", "cmd": "/bin/zsh" } ], "default": "/bin/zsh", "cwd": "/home/user"}| Field | Type | Description |
|---|---|---|
name |
string | Display name of the shell |
path |
string | Full path to the shell executable |
cmd |
string | Original command name (on Windows this differs from the full path) |
default |
string | Path to the server’s default shell |
cwd |
string | Server’s default working directory |
---
#### `GET /api/share-token`
Generate a fresh share token for sharing access. The token is single-use and expires after 5 minutes. Requires authentication.
**Response (200):**
```json{ "url": "https://your-tunnel-url/?ott=<token>" }The returned URL auto-logs in whoever opens it. The token is consumed on first use and expires after 5 minutes. When accessed through a tunnel, the URL uses the public tunnel address.
Response (404):
{ "error": "auth disabled" }Returned when the server was started with --no-password.
Utilities
Section titled “Utilities”GET /api/version
Section titled “GET /api/version”Get the server version. Recomputed per request so dev source checkouts surface new commits and dirty state without a restart. Production installs short-circuit to the cached package.json value, so the cost is negligible.
Response:
{ "version": "1.0.0" }GET /api/config
Section titled “GET /api/config”Get public server configuration. No authentication required.
Response:
{ "passwordRequired": true }| Field | Type | Description |
|---|---|---|
passwordRequired |
boolean | Whether the server requires password to access |
GET /api/update-check
Section titled “GET /api/update-check”Check if a newer version of TermBeam is available. TermBeam compares npm with the latest
GitHub Release and uses the newest valid stable version, so the check still works when npm is
blocked or a registry proxy is stale. Results are cached for 24 hours.
Requires authentication (session cookie or Authorization: Bearer <password>) when authentication is enabled. If the server is started with --no-password, this endpoint is accessible without authentication.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
force |
boolean | Bypass cache and fetch fresh data |
Response:
{ "current": "1.10.2", "latest": "1.11.0", "updateAvailable": true, "method": "npm", "command": "npm install -g termbeam@latest", "canAutoUpdate": true, "restartStrategy": "exit"}| Field | Type | Description |
|---|---|---|
method |
string | How TermBeam was installed: npm, npx, yarn, pnpm, docker, or source |
command |
string | Suggested update command for this installation method |
canAutoUpdate |
boolean | Whether the in-app update mechanism can auto-update this installation |
restartStrategy |
string | How the server will restart after update: pm2 (PM2 managed restart), exit (clean exit), or none |
When no update is available or the check fails, updateAvailable is false and latest may be null.
POST /api/update
Section titled “POST /api/update”Trigger an in-app update. Only works when canAutoUpdate is true (npm/yarn/pnpm global installs, and git checkouts running under PM2). Rate-limited to 1 request per 5 minutes.
For a git checkout, the server fast-forwards the branch (git pull --ff-only), runs npm install --no-save only where node_modules no longer matches package-lock.json (the server’s runtime packages, and everything the web app build needs), rebuilds the web app and restarts through PM2. If the npm registry can’t serve a new package, such as a mirror that lags behind npmjs.org, npm rolls back and the update continues while the installed packages still satisfy package.json; the startup log lists what was skipped. A failed install script or a timed-out install stops the update, and the next attempt has to complete an install before it goes on. A failed step reports a one-line error naming the step, for example git pull: Not possible to fast-forward, aborting. Local commits block a fast-forward; update this checkout by hand.
Response (success):
{ "status": "updating", "method": "npm"}Response (not supported):
{ "error": "Auto-update not available for this installation method", "method": "source", "command": "git pull && npm install && npm run build:frontend", "canAutoUpdate": false}Response (already in progress):
Returns HTTP 409 with the current update state.
Response (rate-limited):
Returns HTTP 429 when rate limit (1 request per 5 minutes) is exceeded:
{ "error": "Update already attempted recently. Try again in a few minutes."}After triggering, progress is broadcast via WebSocket update-progress messages. The server will either restart (PM2) or exit cleanly (non-PM2) once the update is verified.
GET /api/update/status
Section titled “GET /api/update/status”Poll the current update status. Useful as a fallback when WebSocket is not connected.
Response:
{ "status": "idle", "phase": null, "progress": null, "error": null, "fromVersion": null, "toVersion": null, "startedAt": null, "restartStrategy": null}| Status | Description |
|---|---|
idle |
No update in progress |
checking-permissions |
Verifying write access to global npm prefix |
installing |
Running package manager install command |
verifying |
Checking installed version after update |
restarting |
Update verified, server restarting |
complete |
Update finished successfully |
failed |
Update failed (see error field) |
GET /api/dirs?q=/path
Section titled “GET /api/dirs?q=/path”List subdirectories for the folder browser.
Response:
{ "base": "/home/user", "dirs": ["/home/user/projects", "/home/user/documents"], "truncated": false}| Field | Type | Description |
|---|---|---|
base |
string | Absolute path that was listed |
dirs |
array | Absolute paths of subdirectories |
truncated |
boolean | true when results were cut off at the 500-entry limit |
POST /api/upload
Section titled “POST /api/upload”Upload an image file. The request body is the raw image data with the appropriate Content-Type header (e.g., image/png, image/jpeg).
Request headers:
Content-Type: Must be animage/*type
Response (201):
{ "id": "uuid", "url": "/uploads/uuid", "path": "/tmp/termbeam-uuid.png" }Response (400):
{ "error": "Invalid content type" }{ "error": "No image data" }{ "error": "File content does not match declared image type" }Returned when the file’s magic bytes don’t match the declared Content-Type header.
Response (413):
{ "error": "File too large" }Maximum file size is 10 MB.
POST /api/sessions/:id/upload
Section titled “POST /api/sessions/:id/upload”Upload a file to a session’s working directory. The request body is the raw file content. The filename is provided via the X-Filename header and sanitized server-side (path traversal sequences are stripped). Duplicate filenames are auto-renamed (e.g., file (1).txt).
Request headers:
Content-Type: The file’s MIME type (e.g.,application/octet-stream)X-Filename: Original filename (required)X-Target-Dir: Override destination directory (optional, defaults to session cwd)
Response (201):
{ "name": "script.sh", "path": "/home/user/project/script.sh", "size": 1024 }Response (400):
{ "error": "Missing X-Filename header" }{ "error": "Invalid filename" }{ "error": "Empty file" }Response (404):
{ "error": "Session not found" }Response (413):
{ "error": "File too large (max 10 MB)" }GET /uploads/:id
Section titled “GET /uploads/:id”Serve a previously uploaded file by its opaque ID. Requires authentication.
Response: The file content with appropriate content type.
Response (404):
{ "error": "not found" }GET /api/sessions/:id/git/status
Section titled “GET /api/sessions/:id/git/status”Returns parsed git status for a session’s working directory.
Response (200):
{ "branch": "main", "ahead": 3, "behind": 0, "staged": [{ "path": "src/index.js", "status": "M", "oldPath": null }], "modified": [{ "path": "README.md", "status": "M", "oldPath": null }], "untracked": ["new-file.txt"], "isGitRepo": true}| Field | Type | Description |
|---|---|---|
branch |
string | Current branch name |
ahead |
number | Commits ahead of upstream |
behind |
number | Commits behind upstream |
staged |
array | Staged files, each with path, status, and oldPath (for renames) |
modified |
array | Modified files, each with path, status, and oldPath (for renames) |
untracked |
array | Untracked file paths |
isGitRepo |
boolean | Whether the session’s cwd is inside a git repository |
GET /api/sessions/:id/git/diff
Section titled “GET /api/sessions/:id/git/diff”Returns file diff for a specific file.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
file |
string | File path relative to repo root (required) |
staged |
boolean | Show staged changes instead of working tree |
untracked |
boolean | Treat file as untracked (diff against empty) |
context |
number | Number of context lines around changes |
Response (200):
{ "file": "src/index.js", "hunks": [ { "header": "@@ -10,6 +10,7 @@", "oldStart": 10, "oldLines": 6, "newStart": 10, "newLines": 7, "lines": [ { "type": "context", "content": "const express = require('express');", "oldLine": 10, "newLine": 10 }, { "type": "add", "content": "const cors = require('cors');", "oldLine": null, "newLine": 11 } ] } ], "additions": 1, "deletions": 0, "isBinary": false}| Field | Type | Description |
|---|---|---|
file |
string | File path |
hunks |
array | Diff hunks with header, line ranges, and lines |
additions |
number | Total number of added lines |
deletions |
number | Total number of deleted lines |
isBinary |
boolean | Whether the file is binary (no line-level diff) |
Each line in a hunk contains type ("add", "remove", or "context"), content, oldLine, and newLine.
GET /api/sessions/:id/git/blame
Section titled “GET /api/sessions/:id/git/blame”Returns per-line blame information for a file.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
file |
string | File path relative to repo root (required) |
Response (200):
{ "file": "src/index.js", "lines": [ { "line": 1, "content": "const express = require('express');", "commit": "a1b2c3d", "author": "Jane Doe", "date": "2025-01-15T10:30:00.000Z", "summary": "Initial commit" } ]}| Field | Type | Description |
|---|---|---|
file |
string | File path |
lines |
array | Per-line blame entries |
Each line entry contains line (number), content (string), commit (short hash), author, date (ISO 8601), and summary (commit message first line).
GET /api/sessions/:id/git/log
Section titled “GET /api/sessions/:id/git/log”Returns commit log for the repository.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
number | Max commits to return (default 20, max 100) |
file |
string | Filter commits to those touching this file path |
Response (200):
{ "commits": [ { "hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", "shortHash": "a1b2c3d", "author": "Jane Doe", "email": "jane@example.com", "date": "2025-01-15T10:30:00.000Z", "subject": "feat: add git integration", "body": "" } ]}| Field | Type | Description |
|---|---|---|
commits |
array | List of commits |
Each commit contains hash, shortHash, author, email, date (ISO 8601), subject, and body.
Push Notifications
Section titled “Push Notifications”GET /api/push/vapid-key
Section titled “GET /api/push/vapid-key”Returns the VAPID public key needed to create a push subscription on the client.
Response (200):
{ "publicKey": "BNq..." }| Field | Type | Description |
|---|---|---|
publicKey |
string | Base64url-encoded VAPID public key |
POST /api/push/subscribe
Section titled “POST /api/push/subscribe”Register a Web Push subscription with the server.
Request:
{ "subscription": { "endpoint": "https://fcm.googleapis.com/fcm/send/...", "keys": { "p256dh": "BNq...", "auth": "abc..." } }}Response (200):
{ "ok": true }DELETE /api/push/unsubscribe
Section titled “DELETE /api/push/unsubscribe”Remove a previously registered push subscription.
Request:
{ "endpoint": "https://fcm.googleapis.com/fcm/send/..." }Response (200):
{ "ok": true }Tunnel
Section titled “Tunnel”GET /api/tunnel/status
Section titled “GET /api/tunnel/status”Returns the current tunnel connection state, provider, and token lifetime.
Response (200):
{ "state": "connected", "url": "https://abcd1234-3456.euw.devtunnels.ms", "since": 1760000000000, "provider": "microsoft", "tokenLifetimeSeconds": 3200}| Field | Type | Description |
|---|---|---|
state |
string | connected, disconnected, reconnecting, waiting-network, auth-expired, failed, or unknown (no tunnel) |
url |
string | Current public URL (present when connected) |
urlChanged |
boolean | true after the tunnel had to be re-created and the public URL changed |
since |
number | When the state last changed (ms) |
provider |
string/null | Host login provider: microsoft, github, or null — read from a cache refreshed by the watchdog (never blocks) |
tokenLifetimeSeconds |
number/null | Seconds remaining on the host’s tunnel login token, or null if unavailable |
Port Preview
Section titled “Port Preview”GET /preview/:port/*
Section titled “GET /preview/:port/*”Reverse-proxies requests to a service running on 127.0.0.1:<port>. All HTTP methods are supported. The path after the port is forwarded as-is, along with query strings and request headers.
Requires authentication (cookie or Bearer token).
Response: The upstream response is streamed back with its original status code and headers.
Response (400):
{ "error": "Invalid port: must be an integer between 1 and 65535" }Response (502):
{ "error": "Bad gateway: upstream server is not responding" }Returned when the upstream service is unreachable or the connection is refused.
Response (504):
{ "error": "Gateway timeout: upstream server did not respond in time" }Returned when the upstream service does not respond within 10 seconds.
AI Agents API
Section titled “AI Agents API”List Detected Agents
Section titled “List Detected Agents”GET /api/agentsReturns AI coding agents detected in the server’s PATH.
Response:
{ "agents": [ { "id": "copilot", "name": "GitHub Copilot", "cmd": "copilot", "args": [], "icon": "copilot", "version": "1.2.3" } ]}Results are cached for 60 seconds. If both copilot and gh copilot are available, only the standalone copilot is returned.
List Copilot Models
Section titled “List Copilot Models”GET /api/copilot/modelsReturns the models the server’s signed-in Copilot account can use, straight from the Copilot SDK — so newly released models appear, and retired ones disappear, without a TermBeam update. Models disabled by the account’s policy are left out. The list is cached for 30 minutes; after that the cached copy is returned immediately while a fresh one loads in the background. The first call may take a few seconds while the Copilot CLI starts.
Response:
{ "available": true, "models": [ { "id": "auto", "name": "Auto" }, { "id": "claude-sonnet-5", "name": "Claude Sonnet 5" }, { "id": "gpt-5.5", "name": "GPT-5.5" } ]}available is false (with an empty list) when the Copilot SDK isn’t installed. If Copilot can’t be reached, the response is { "available": true, "models": [], "error": "Could not load models from Copilot" } — clients should then offer only Copilot’s default.
List Past Agent Sessions
Section titled “List Past Agent Sessions”GET /api/agent-sessions?limit=100&agent=copilot&search=authReturns past sessions from Copilot (SQLite) and Claude Code (JSONL) for resume.
| Parameter | Default | Description |
|---|---|---|
limit |
100 |
Max sessions to return (1–500) |
agent |
all | Filter by agent: copilot or claude |
search |
— | Search in summary, CWD, repo, branch |
Response:
{ "sessions": [ { "id": "35b1faca-...", "agent": "copilot", "agentName": "GitHub Copilot", "agentIcon": "copilot", "summary": "Review Copilot Session Portal", "cwd": "/Users/dev/projects/termbeam", "repo": "user/repo", "branch": "main", "updatedAt": "2026-04-04T10:11:01.609Z", "turnCount": 23 } ]}Sessions with zero user turns are excluded. Copilot session reading requires better-sqlite3 (listed in optionalDependencies — installs automatically when native build tools are available, gracefully skipped otherwise).
Get Resume Command
Section titled “Get Resume Command”GET /api/agent-sessions/:agent/:id/resume-commandReturns the CLI command to resume a specific agent session.
Response:
{ "command": "copilot --resume=35b1faca-..." }WebSocket API
Section titled “WebSocket API”Connect to ws://host:port/ws.
Message Types (Client → Server)
Section titled “Message Types (Client → Server)”Authenticate
Section titled “Authenticate”If the server has a password set and the WebSocket connection wasn’t authenticated via cookie, send an auth message first. When the server is started with --no-password, authentication is skipped automatically.
{ "type": "auth", "password": "your-password" }or with an existing token:
{ "type": "auth", "token": "session-token" }Response:
{ "type": "auth_ok" }Auth Failure:
{ "type": "error", "code": "unauthorized", "message": "Unauthorized" }Handshakes from a browser origin TermBeam doesn’t recognise (see Cross-site protection) are closed with code 1008 (Origin not allowed / Invalid origin) before any message is exchanged.
After an auth failure the connection is closed with code 4401. Sending any non-auth message before authenticating has the same result. The same message ("Session expired" / "Signed out") and close code are used when a cookie-authenticated socket’s session expires or is revoked while it is open — clients should stop reconnecting and ask the user to unlock.
Keepalive
Section titled “Keepalive”{ "type": "ping", "t": 1760000000000 }Answered with { "type": "pong", "t": 1760000000000 } (the same t), which clients use to measure round-trip time and to detect dead (“zombie”) sockets. The server additionally sends protocol-level pings every 15 seconds.
Attach to Session
Section titled “Attach to Session”{ "type": "attach", "sessionId": "a1b2c3d4" }After a successful attach, when the session has buffered output, the server sends a single replay message containing the session’s sanitized scrollback buffer (up to ~1,000,000 characters; trimmed back to ~500,000 when it grows beyond that), followed by the attached confirmation. Clients should treat replay as an authoritative state snapshot and reset their terminal before applying it — this prevents duplicated content on reconnect when the terminal UI is preserved across socket lifecycles.
Send Input
Section titled “Send Input”{ "type": "input", "data": "ls -la\r" }Resize Terminal
Section titled “Resize Terminal”{ "type": "resize", "cols": 120, "rows": 30 }The server validates resize dimensions: cols must be between 1–500 and rows between 1–200. Values outside these bounds are ignored.
Message Types (Server → Client)
Section titled “Message Types (Server → Client)”Terminal Output
Section titled “Terminal Output”{ "type": "output", "data": "..." }Replay Snapshot
Section titled “Replay Snapshot”Sent on attach. Contains sanitized scrollback, an alt-screen re-entry sequence when the session is currently in alt-screen, and finally the terminal input modes the running program has enabled (mouse tracking and encoding, application cursor keys, bracketed paste, focus reporting, cursor visibility, auto-wrap). The modes are tracked server-side as output flows, so they are restored even when the original escape sequences have been trimmed from the scrollback — otherwise full-screen apps such as GitHub Copilot CLI would receive arrow keys instead of mouse-wheel events after a reconnect. Clients should drop pending writes, reset their terminal, and write this payload — do not append it on top of preserved terminal content.
{ "type": "replay", "data": "..." }Attached Confirmation
Section titled “Attached Confirmation”{ "type": "attached", "sessionId": "a1b2c3d4" }Session Exited
Section titled “Session Exited”{ "type": "exit", "code": 0 }Notification
Section titled “Notification”Sent when a command completes (child process exits). Broadcast in real time to connected clients and replayed on attach for events that occurred while disconnected.
{ "type": "notification", "notificationType": "command-complete", "sessionName": "my-project", "timestamp": 1719849600000}| Field | Type | Description |
|---|---|---|
notificationType |
string | Notification kind (command-complete) |
sessionName |
string | Name of the session where the event fired |
timestamp |
number | Unix timestamp (ms) when the event occurred |
{ "type": "error", "message": "Session not found" }Update Progress
Section titled “Update Progress”Sent during an in-app update (triggered via POST /api/update). Allows the frontend to show real-time progress without polling.
{ "type": "update-progress", "status": "installing", "phase": "Installing update...", "progress": "added 42 packages...", "error": null, "fromVersion": "1.14.0", "toVersion": null, "restartStrategy": "exit"}The status field follows the same values as GET /api/update/status. Git checkouts move through the phase values Pulling the latest code..., Installing dependencies..., Installing web app dependencies... and Building the web app... (install phases appear only when needed). When status reaches restarting, the WebSocket connection will close shortly after (close code 1012 for non-PM2 installs).
Tunnel Status
Section titled “Tunnel Status”Broadcast to authenticated clients when the host’s tunnel changes state (useful for LAN/localhost clients that can still reach the server while remote access is down).
{ "type": "tunnel-status", "state": "connected", "url": "https://wxyz5678-3456.euw.devtunnels.ms", "urlChanged": true, "since": 1760000000000}| Field | Type | Description |
|---|---|---|
state |
string | connected, disconnected, reconnecting, waiting-network, auth-expired (host login), or failed |
url |
string | Public URL (when connected) |
urlChanged |
boolean | The tunnel was re-created and the URL changed |
attempt |
number | Restart attempt number (when reconnecting) |
since |
number | When the state changed (ms) |
See Also
Section titled “See Also”- Architecture — system design, module responsibilities, and data flow
- Security — threat model, safe usage, and security features