Console API Reference

The Console node's HTTP service listens on :8089. All REST endpoints use the /api/v1 prefix.

Base URL: http://<console-host>:8089

REST prefix: /api/v1

Content type: application/json

Timestamps: RFC3339

ID format: Opaque strings (acc_*, tok_*, worker UUID, task ID)

Unified error response format:

{ "error": "message" }

Auth Model

The Console exposes five credential types for different use cases:

ItemCookie sessionAPI Key (Bearer)Dashboard JIT (Bearer)Execution Token (Bearer)MCP JIT (Bearer)
Credential typeCookie onlyboxes_console_sessionAuthorization: Bearer obxk_*Authorization: Bearer obx_dashboard_jit_v1.*Authorization: Bearer obx_*Authorization: Bearer obx_jit_v1.*
OriginPOST /api/v1/auth/loginPOST /api/v1/api-keys (cookie only)Externally signed (HMAC-SHA256, CONSOLE_DASHBOARD_JIT_SIGNING_KEY)POST /api/v1/tokens (cookie only)Externally signed (HMAC-SHA256, CONSOLE_JIT_SIGNING_KEY)
Lifetime12 hours (in-memory; invalidated on console restart)Persistent until manually deletedStateless; expires via payload exp or by rotating the signing keyPersistent until manually deletedStateless; revoke by rotating the signing key
Applicable endpointsAll management endpointsMost management endpoints (read + selective write)Selected management APIs for worker-sys provisioning/api/v1/commands/*, /api/v1/tasks*, /mcp/api/v1/commands/*, /api/v1/tasks*, /mcp
Primary useWeb UI managementProgrammatic managementServer-to-server dashboard automation without MCP accessCommand execution and MCPThird-party execution integrations without pre-created accounts

The following endpoints accept cookie session only and reject API Key Bearer and Dashboard JIT: POST /api/v1/auth/password, POST /api/v1/api-keys, DELETE /api/v1/api-keys/:api_key_id, and all /api/v1/tokens* endpoints.

If no execution tokens exist in the system, trusted-token auth for command execution endpoints and /mcp returns 401; valid MCP JIT tokens can still authenticate when CONSOLE_JIT_SIGNING_KEY is configured.

MCP JIT tokens (obx_jit_v1.*) are explicitly rejected on all dashboard/management routes. Dashboard JIT tokens (obx_dashboard_jit_v1.*) are explicitly rejected by /mcp.


REST API

The Web UI and third-party integrations use the same resource-oriented endpoints. Each endpoint enforces the management or execution credential requirements documented below.

Previously published /api/v1/console/* authentication, account, API key, and token paths remain available as compatibility aliases. New integrations should use the canonical paths shown below. Terminal session management has no /api/v1/console/sessions* alias because it was first published at /api/v1/sessions*.

Authentication and Accounts

Login

POST /api/v1/auth/login

Request body:

{
  "username": "admin",
  "password": "secret"
}
FieldTypeRequiredNotes
usernamestringYesAccount username
passwordstringYesAccount password

Response 200:

{
  "authenticated": true,
  "account": {
    "account_id": "acc_xxx",
    "username": "admin",
    "is_admin": true
  },
  "registration_enabled": false,
  "console_version": "v0.0.0",
  "console_repo_url": "https://..."
}

Also sets the onlyboxes_console_session cookie in the response.

Error codes:

StatusDescription
400Malformed JSON
401Incorrect username or password
500Session creation failure

Get Session

GET /api/v1/auth/session

Accepts Dashboard Cookie, Console API Key, or Dashboard JIT authentication. The response structure is identical to the login response.

Error codes:

StatusDescription
401Not logged in or session expired

Logout

POST /api/v1/auth/logout

Clears the response cookie and destroys the in-memory session (if present).

Response: 204 No Content


Register Account (Admin only)

POST /api/v1/accounts

Creates a new non-admin account. Requires an authenticated administrator and the registration toggle (CONSOLE_ENABLE_REGISTRATION) must be true.

Request body:

{
  "username": "dev-user",
  "password": "strong-password"
}
FieldTypeRequiredConstraints
usernamestringYesTrimmed non-empty, length ≤ 64, case-insensitively unique system-wide
passwordstringYesNon-empty

Response 201:

{
  "account": {
    "account_id": "acc_xxx",
    "username": "dev-user",
    "is_admin": false
  },
  "created_at": "2026-02-21T00:00:00Z",
  "updated_at": "2026-02-21T00:00:00Z"
}

Error codes:

StatusDescription
400Empty username / length > 64 / empty password
403Registration disabled (CONSOLE_ENABLE_REGISTRATION=false) or caller is not admin
409Username conflict
500Database or internal error

Change Password

POST /api/v1/auth/password

Changes the current account's password. All active sessions for the account are rotated after a successful change.

Request body:

{
  "current_password": "old-password",
  "new_password": "new-password"
}
FieldTypeRequiredNotes
current_passwordstringYesCurrent password for verification
new_passwordstringYesNew password

Response:

StatusDescription
204Password changed successfully
400Malformed JSON / missing field
401Current password incorrect
500Internal error

List Accounts (Admin only)

GET /api/v1/accounts

Query parameters:

ParamTypeDefaultConstraints
pageint1Positive integer
page_sizeint20Positive integer, max 100

Response 200:

{
  "items": [
    {
      "account_id": "acc_xxx",
      "username": "admin",
      "is_admin": true,
      "created_at": "2026-02-21T00:00:00Z",
      "updated_at": "2026-02-21T00:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 20
}

Error codes:

StatusDescription
400Invalid query parameters
403Caller is not admin
500Database or internal error

Delete Account (Admin only)

DELETE /api/v1/accounts/:account_id

Path parameters:

ParamDescription
account_idAccount ID (acc_*)

Response:

StatusDescription
204Deleted successfully
403Cannot delete the currently logged-in account / cannot delete an admin account
404Account not found
500Internal error

Token Management

Tokens are scoped per account; each account can only manage its own tokens.

All token management endpoints require a Cookie session. API keys and Dashboard JIT tokens are rejected so dashboard bearer credentials cannot mint or manage MCP trusted tokens.

List Tokens

GET /api/v1/tokens

Response 200:

{
  "items": [
    {
      "id": "tok_xxx",
      "name": "default-token",
      "token_masked": "obx_******abcd",
      "created_at": "2026-02-21T00:00:00Z",
      "updated_at": "2026-02-21T00:00:00Z"
    }
  ],
  "total": 1
}

token_masked shows only the last four characters. The plaintext token is only returned at creation time.


Create Token

POST /api/v1/tokens

Request body:

{
  "name": "ci-prod",
  "token": "optional-manual-token"
}
FieldTypeRequiredConstraints
namestringYesTrimmed non-empty, length ≤ 64, case-insensitively unique within the account
tokenstringNoOmit to auto-generate (obx_<hex>); if provided, must contain no whitespace, length ≤ 256

Response 201:

{
  "id": "tok_xxx",
  "name": "ci-prod",
  "token": "obx_plaintext_or_manual",
  "token_masked": "obx_******abcd",
  "generated": true,
  "created_at": "2026-02-21T00:00:00Z",
  "updated_at": "2026-02-21T00:00:00Z"
}

The token field (plaintext) is only returned in this response and cannot be retrieved again. Save it immediately.

Error codes:

StatusDescription
400Validation failure
409Token name conflict or token value conflict
500Internal error

Delete Token

DELETE /api/v1/tokens/:token_id

Path parameters:

ParamDescription
token_idToken ID (tok_*)

Response:

StatusDescription
204Deleted successfully
404Token not found (including cross-account access)

Get Token Plaintext

GET /api/v1/tokens/:token_id/value

This endpoint always returns 410 Gone. Token plaintext is only returned at creation time.

{
  "error": "token value is only returned at creation time; delete and recreate the token to obtain a new value"
}

API Key Management

API Keys are for programmatic access to most management endpoints. Unlike execution tokens, they cannot be used for command execution endpoints or /mcp. Each account can only manage its own API keys.

List API Keys

GET /api/v1/api-keys

Auth: Cookie session or API Key Bearer

Response 200:

{
  "items": [
    {
      "id": "apik_xxx",
      "name": "my-key",
      "key_masked": "obxk_******abcd",
      "created_at": "2026-02-21T00:00:00Z",
      "updated_at": "2026-02-21T00:00:00Z"
    }
  ],
  "total": 1
}

Create API Key

POST /api/v1/api-keys

Auth: Cookie session only

Request body:

{ "name": "ci-management" }
FieldTypeRequiredConstraints
namestringYesTrimmed non-empty, length ≤ 64, case-insensitively unique within the account

Response 201:

{
  "id": "apik_xxx",
  "name": "ci-management",
  "key": "obxk_plaintext",
  "key_masked": "obxk_******abcd",
  "created_at": "2026-02-21T00:00:00Z",
  "updated_at": "2026-02-21T00:00:00Z"
}

The key field (plaintext) is only returned in this response and cannot be retrieved again. Save it immediately.

Error codes:

StatusDescription
400Malformed body / empty name / name exceeds 64 characters
401Missing or invalid cookie session (API Key Bearer not accepted)
409Name conflict
503Storage unavailable
500Internal error

Delete API Key

DELETE /api/v1/api-keys/:api_key_id

Auth: Cookie session only

Path parameters:

ParamDescription
api_key_idAPI Key ID (apik_*)

Response:

StatusDescription
204Deleted successfully
400Missing api_key_id
401Missing or invalid cookie session (API Key Bearer not accepted)
404API Key not found (including cross-account access)
500Internal error

Worker Management

Permission Matrix

OperationAdminRegular user
List workersAll workersOwn worker-sys only
StatsAll workersOwn worker-sys only
InflightAll workersOwn worker-sys only
Create normalYesNo
Create worker-sysYes (multiple per account)Yes (multiple per account)
Delete any workerYesNo
Delete own worker-sysYesYes

Worker types: normal (backed by worker-docker/worker-boxlite), worker-sys (backed by worker-sys).


List Workers

GET /api/v1/workers

Query parameters:

ParamTypeDefaultOptions
pageint1Positive integer
page_sizeint20Positive integer, max 100
statusstringallall / online / offline

Response 200:

{
  "items": [
    {
      "node_id": "worker-1",
      "node_name": "node-a",
      "executor_kind": "docker",
      "capabilities": [
        { "name": "echo", "max_inflight": 4 }
      ],
      "labels": {
        "region": "us",
        "obx.owner_id": "acc_xxx",
        "obx.worker_type": "normal"
      },
      "version": "v0.1.0",
      "status": "online",
      "registered_at": "2026-02-21T00:00:00Z",
      "last_seen_at": "2026-02-21T00:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 20
}

Error codes:

StatusDescription
400Invalid query parameters

Worker Stats

GET /api/v1/workers/stats

Query parameters:

ParamTypeDefaultDescription
stale_after_secint30Seconds threshold after which a worker is considered stale (no heartbeat)

Response 200:

{
  "total": 5,
  "online": 4,
  "offline": 1,
  "stale": 1,
  "stale_after_sec": 30,
  "generated_at": "2026-02-21T00:00:00Z"
}

For regular users, stats only reflect their own worker-sys.


Worker Inflight

GET /api/v1/workers/inflight

Response 200:

{
  "workers": [
    {
      "node_id": "worker-1",
      "active_session_count": 3,
      "terminal_session_capacity": {
        "known": true,
        "max_active_sessions": 4
      },
      "capabilities": [
        { "name": "pythonExec", "inflight": 1, "max_inflight": 4 }
      ]
    }
  ],
  "generated_at": "2026-02-21T00:00:00Z"
}

For regular users, results only include their own worker-sys.

  • active_session_count is the worker's latest terminal reservation count.
  • terminal_session_capacity.known=false means a legacy worker did not declare a maximum; it must not be interpreted as unlimited.
  • known=true,max_active_sessions=0 explicitly means unlimited active terminal sessions.

Create Worker Credentials

POST /api/v1/workers

Request body:

{
  "type": "normal"
}
FieldTypeRequiredOptions
typestringYesnormal / worker-sys

Response 201:

{
  "node_id": "2f51f8f9-77f2-4c1a-a4f5-2036fc9fcb9e",
  "type": "normal",
  "worker_secret": "a3f8c2..."
}

worker_secret is returned only once in this response and cannot be retrieved again.

Error codes:

StatusDescription
400Malformed body / invalid type value
403Regular user attempting to create a normal worker
503Provisioning service unavailable
500Creation failure

Delete Worker

DELETE /api/v1/workers/:node_id

Path parameters:

ParamDescription
node_idWorker node ID (UUID)

Response:

StatusDescription
204Deleted successfully
400Missing node_id
404Worker not found (cross-account access by regular users also returns 404)
503Provisioning service unavailable

Get Worker Startup Command

GET /api/v1/workers/:node_id/startup-command

This endpoint always returns 410 Gone. The startup command (including the secret) is only returned when the worker is created.

{
  "error": "worker secret is returned only when creating the worker; delete and recreate to get a new startup command"
}

Terminal Session API

These endpoints require management authentication (cookie, Console API key, or Dashboard JIT). They list, inspect, renew, and delete confirmed terminal sessions. Sessions that have not finished their first terminalExec are not visible.

Scope:

  • non-admin: only the caller's sessions; account_id for another account returns 404
  • admin: all sessions by default; account_id filters the list. Get, renew, and delete require account_id because session_id is unique per account, not globally

status values:

  • ready: the bound worker is dispatchable
  • unavailable: the bound worker is disconnected; the session keeps its worker and lease
  • reconciling: the bound worker is reconnecting and checking the session resource

Deleting a session removes Console's route and any public preview routes for that session immediately. The worker sandbox is reclaimed when its lease expires.

List Sessions

GET /api/v1/sessions

Query parameters:

ParameterTypeDefaultConstraints
pageint1Positive integer
page_sizeint20Positive integer, max 100
account_idstringOptional. Admin uses it to filter; non-admin may only pass their own account

Response 200:

{
  "items": [
    {
      "account_id": "acc_xxx",
      "session_id": "sess_xxx",
      "worker_id": "worker-1",
      "status": "ready",
      "lease_expires_at": "2026-02-21T00:01:00Z",
      "last_used_at": "2026-02-21T00:00:30Z",
      "created_at": "2026-02-21T00:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 20
}

Error codes:

StatusDescription
400Invalid query
401Missing or invalid dashboard credential
404Non-admin requested another account
503Session registry unavailable

Get Session

GET /api/v1/sessions/:session_id?account_id=acc_xxx

Success 200: same object as a list item.

Error codes:

StatusDescription
400Admin omitted account_id
401Missing or invalid dashboard credential
404Session not found (including cross-account)
503Session registry unavailable

Renew Session Lease

POST /api/v1/sessions/:session_id/renew?account_id=acc_xxx

Console asks the Worker that owns the session to extend its lease, then persists the absolute expiry confirmed by that Worker. Renewal is monotonic: a shorter TTL does not reduce the current expiry. Renewing a session does not extend any public preview route.

{
  "lease_ttl_sec": 300
}

lease_ttl_sec must be positive and within the owning Worker's configured terminal lease bounds.

Success 200: same object as a list item, with updated lease_expires_at and last_used_at values.

Error codes:

StatusDescription
400Invalid body, admin omitted account_id, or TTL is outside the Worker's lease bounds
401Missing or invalid dashboard credential
404Session not found (including cross-account)
502Worker rejected the renewal or returned an invalid result
503Session registry or bound Worker unavailable
504Renewal timed out

Delete Session

DELETE /api/v1/sessions/:session_id?account_id=acc_xxx

Response:

StatusDescription
204Deleted
400Admin omitted account_id
401Missing or invalid dashboard credential
404Session not found (including cross-account)
500Failed to delete
503Session registry unavailable

Public Preview Routes

These management APIs accept Dashboard cookie, Console API key, or Dashboard JIT authentication. Routes are account-scoped and persisted in SQLite. See Public Preview Proxy for the full deployment guide.

POST /api/v1/proxy-routes

{
  "session_id": "sess_xxx",
  "port": 8080
}

Success 201:

{
  "route_key": "ceirceirceirceirceirceirce",
  "account_id": "acc_xxx",
  "session_id": "sess_xxx",
  "port": 8080,
  "url": "https://ceirceirceirceirceirceirce.public-preview.example.com",
  "created_at": "2026-02-21T00:00:00Z",
  "expires_at": "2026-02-22T00:00:00Z"
}

Rules and errors:

  • session_id must belong to the current account and have a confirmed route to an online proxy-enabled Docker, Boxlite, or E2B Worker.
  • port must be 1..65535.
  • route URLs use CONSOLE_PROXY_PUBLIC_SCHEME (https by default) and CONSOLE_PROXY_PUBLIC_BASE_DOMAIN; use http only for trusted local development.
  • route keys use lowercase Base32 and default to 26 characters. CONSOLE_PROXY_ROUTE_KEY_LENGTH controls the length of newly generated keys in the range 8..26; existing routes remain valid after it changes. Values below 16 reduce resistance to URL guessing and are intended only for trusted local or low-risk deployments.
  • each account can hold at most 16 active routes by default; configure it with CONSOLE_PROXY_ROUTE_MAX_PER_ACCOUNT.
  • each session can hold at most 2 active routes by default; configure it with CONSOLE_PROXY_ROUTE_MAX_PER_SESSION.
  • default route TTL is 86400 seconds, can be changed with CONSOLE_PROXY_ROUTE_TTL_SEC, and cannot exceed 604800 seconds (7 days).
  • 400 invalid body, session ID, or port.
  • 404 session route not found.
  • 429 account or session route limit reached.
  • 503 the session's Worker has no available proxy endpoint.

GET /api/v1/proxy-routes?page=1&page_size=20&account_id=acc_xxx

Scope:

  • non-admin accounts can list only their own routes; setting account_id to another account returns 404.
  • admins list routes across all accounts by default and can use account_id to filter one account.

Query parameters:

ParameterTypeDefaultConstraints
pageint1Positive integer
page_sizeint20Positive integer, max 100
account_idstringOptional account filter as described above

Success 200:

{
  "items": [
    {
      "route_key": "ceirceirceirceirceirceirce",
      "account_id": "acc_xxx",
      "session_id": "sess_xxx",
      "port": 8080,
      "url": "https://ceirceirceirceirceirceirce.public-preview.example.com",
      "created_at": "2026-02-21T00:00:00Z",
      "expires_at": "2026-02-22T00:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 20
}

Error codes:

StatusDescription
400Invalid page or page_size
401Missing or invalid management credential
404Non-admin requested another account's routes

DELETE /api/v1/proxy-routes/:route_key

  • 204 deleted.
  • 404 missing, expired, or owned by another account.

The returned preview URL is anonymous: anyone holding it can access the Sandbox service without an Onlyboxes Cookie or Bearer token. Nginx resolves every new HTTP request through the protected internal Console endpoint. Docker and Boxlite receive a fresh 15-second Route Token for the Worker hop; E2B returns the current sandbox origin and an internal traffic token so the data path is browser → Nginx → E2B. Token expiry does not end an accepted HTTP/SSE/WebSocket connection. Route access does not renew the Sandbox lease. Active routes and their original URLs survive a Console restart; new requests remain unavailable until the owning Worker reconnects and the terminal session finishes recovery.


Command Execution API

The following endpoints use Bearer token auth (Authorization: Bearer <token>) and are intended for external programs, AI agents, and MCP clients.

For /mcp only, clients that cannot set custom headers may pass the same token as a URL query parameter: POST /mcp?token=<token>. Bearer headers remain the recommended path and take precedence when present; command execution and task REST endpoints do not accept query-token auth. The query parameter name defaults to token and can be changed with CONSOLE_MCP_TOKEN_QUERY_PARAM.

Command Execution

Echo

POST /api/v1/commands/echo

Tests connectivity and verifies an echo worker is reachable.

Request body:

{
  "message": "hello",
  "timeout_ms": 5000
}
FieldTypeRequiredConstraints
messagestringYesTrimmed non-empty
timeout_msintNo1..60000, default 5000

Response 200:

{ "message": "hello" }

Error codes:

StatusDescription
400Malformed body / missing message / timeout out of range
429No available concurrency capacity
503No online echo worker
504Execution timed out
502Execution or internal error

Terminal Execution

POST /api/v1/commands/terminal

Executes a shell command inside a persistent terminal session, supporting state reuse across requests.

Request body:

{
  "command": "pwd",
  "session_id": "optional-session",
  "create_if_missing": false,
  "lease_ttl_sec": 60,
  "timeout_ms": 60000,
  "request_id": "optional-idempotency-key"
}
FieldTypeRequiredConstraints
commandstringYesTrimmed non-empty
session_idstringNoExisting session ID; omit to auto-create a new session
create_if_missingboolNoAuto-create if session_id not found, default false
lease_ttl_secintNoSession lease duration in seconds; clamped to worker's configured [min, max] range (default min/default: 60, default max: 1800)
timeout_msintNo1..600000, default 60000
request_idstringNoIdempotency key, scoped per account

When create_if_missing=true, a session_id beginning with the reserved Computer Use prefix (CU: by default) is rejected. With create_if_missing=false, it remains an ordinary sandbox-session lookup.

Response 200:

{
  "session_id": "sess_xxx",
  "created": true,
  "stdout": "...",
  "stderr": "...",
  "exit_code": 0,
  "stdout_truncated": false,
  "stderr_truncated": false,
  "lease_expires_unix_ms": 1770000000000
}
FieldDescription
session_idSession ID used (new or existing)
createdWhether the session was newly created
stdout / stderrCommand output
exit_codeCommand exit code
stdout_truncated / stderr_truncatedWhether output was truncated
lease_expires_unix_msSession lease expiry (Unix millisecond timestamp)

Error codes:

StatusError codeDescription
400invalid_payloadInvalid request parameters
404session_not_foundsession_id not found and create_if_missing=false
409session_busySession occupied by another request, or task cancelled
429no_capacityThe selected worker has exhausted the capability's concurrency quota
429session_capacity_exceededThe selected sandbox worker has reached WORKER_TERMINAL_MAX_ACTIVE_SESSIONS; existing sessions remain usable, but this request cannot create a new session
503session_unavailableThe session's bound worker is offline or reconciling after restart; retry the same session_id without rerouting it
503—No available worker
504—Execution timed out
502—Other execution failure

Computer Use Execution

POST /api/v1/commands/computer-use

Lists the calling account's Worker Systems or executes a shell command on one selected worker-sys host. Execution is pinned to the requested Worker and never falls back.

Request body:

{
  "command": "pwd",
  "worker_id": "worker-id",
  "timeout_ms": 60000,
  "request_id": "optional-idempotency-key"
}
FieldTypeRequiredConstraints
worker_idstringNoCaller-owned worker-sys; omit for list mode
commandstringConditionalRequired and non-empty when worker_id is present
timeout_msintNo1..600000, default 60000
request_idstringNoIdempotency key, scoped per account

Response 200:

Without worker_id, Console performs no Worker dispatch and returns all caller-owned online and offline Worker Systems:

{"worker_list":[{"worker_id":"worker-id","node_name":"laptop","status":"online","capabilities":[]}]}

With worker_id, the response is:

{
  "stdout": "...",
  "stderr": "...",
  "exit_code": 0,
  "stdout_truncated": false,
  "stderr_truncated": false
}

Unlike Terminal Execution, Computer Use has no session (session_id absent). Each command runs directly on the selected host; there is no container isolation.

Error codes:

StatusError codeDescription
400invalid_payloadInvalid request parameters
409session_busyWorker busy or task cancelled
429no_capacityNo available concurrency capacity
503—Selected Worker is offline or lacks the capability
504—Execution timed out
502—Other execution failure

Sandbox Metadata API

Sandbox metadata is account-scoped by bearer token. Execution tokens and MCP JIT tokens can call this endpoint.

Get Sandbox Metadata

GET /api/v1/sandbox/metadata

Response 200:

{
  "provider": "onlyboxes",
  "api_version": "2026-05-25",
  "console": { "version": "0.6.1" },
  "limits": {
    "max_task_timeout_ms": 600000,
    "max_task_wait_ms": 60000,
    "max_terminal_timeout_ms": 600000,
    "default_terminal_lease_sec": 60,
    "max_terminal_lease_sec": 86400
  },
  "capabilities": [
    {
      "name": "terminalExec",
      "available": true,
      "online_nodes": 1,
      "max_inflight": 4
    }
  ],
  "workers": {
    "total": 1,
    "online": 1,
    "offline": 0,
    "stale": 0
  }
}

Capability scope:

CapabilityScope
computerUse, readImageCaller-owned worker-sys only
terminalExec, terminalResource, pythonExec, echoGlobal online worker availability

Error codes:

StatusDescription
401Missing or invalid bearer token
503Worker registry unavailable

Task API

Tasks are suited for asynchronous execution or polling workflows. Ownership is strictly scoped to the token's owning account.

Submit Task

POST /api/v1/tasks

Request body:

{
  "capability": "pythonExec",
  "input": { "code": "print(1)" },
  "mode": "auto",
  "wait_ms": 1500,
  "timeout_ms": 60000,
  "request_id": "optional-idempotency-key"
}
FieldTypeRequiredConstraints
capabilitystringYesNon-empty; matches a capability name declared by a worker (case-insensitive; normalized to lowercase in storage and responses)
inputobjectNoValid JSON object; defaults to {} if omitted
modestringNosync / async / auto, default auto
wait_msintNo1..60000, default 1500; max milliseconds to wait in auto mode
timeout_msintNo1..600000, default 60000; total task timeout
request_idstringNoIdempotency key, scoped per account

Capability-specific routing is identical to MCP and the dedicated command endpoints:

  • computerUse reads input.worker_id; when omitted, the task completes locally with result.worker_list and no Worker dispatch.
  • readImage uses input.session_id="CU:<worker_id>" to target a caller-owned Worker System. Other values are sandbox terminal session IDs.
  • terminalExec rejects create_if_missing=true when input.session_id starts with the reserved Computer Use prefix.

CU: is the default, case-sensitive Computer Use session ID prefix. It can be changed with the CONSOLE_COMPUTER_USE_SESSION_ID_PREFIX environment variable or the computer_use_session_id_prefix TOML setting; the example above uses the default. Console uses the same setting to interpret session IDs in the task API, MCP readImage/exportFile, and terminalExec.

One task keeps the same task_id and request_id across internal terminal capacity retries; command_id is the current or last worker dispatch attempt, so it can change while the task is running.

mode field:

modeBehavior
syncWait synchronously until the task completes
asyncReturn 202 immediately without waiting
autoWait up to wait_ms; return 202 if not yet complete

Response — task still running 202:

{
  "task_id": "task_xxx",
  "request_id": "req-1",
  "command_id": "cmd_xxx",
  "capability": "pythonexec",
  "status": "running",
  "created_at": "2026-02-21T00:00:00Z",
  "updated_at": "2026-02-21T00:00:01Z",
  "deadline_at": "2026-02-21T00:01:00Z",
  "status_url": "/api/v1/tasks/task_xxx"
}

Response — task completed successfully 200:

{
  "task_id": "task_xxx",
  "capability": "pythonexec",
  "status": "succeeded",
  "result": {
    "output": "1\n",
    "stderr": "",
    "exit_code": 0
  },
  "created_at": "2026-02-21T00:00:00Z",
  "updated_at": "2026-02-21T00:00:01Z",
  "deadline_at": "2026-02-21T00:01:00Z",
  "completed_at": "2026-02-21T00:00:01Z"
}

Task error field (present on terminal failure):

{
  "error": {
    "code": "execution_failed",
    "message": "..."
  }
}

Terminal response codes:

StatusDescription
200Task succeeded
409Task was cancelled
504Task timed out
429Failed, error.code=no_capacity or error.code=session_capacity_exceeded
503Failed, error.code=no_worker
502Failed (other error code)

Submission-phase error codes:

StatusDescription
400Invalid parameters / invalid mode / malformed body
409request_id already in progress
429No available concurrency capacity
503No worker with matching capability
504Request timed out
502Submission failure

Get Task

GET /api/v1/tasks/:task_id

Path parameters:

ParamDescription
task_idTask ID

Response:

StatusDescription
200Returns task snapshot (see Submit Task response structure)
404Task not found (including cross-account access)

Cancel Task

POST /api/v1/tasks/:task_id/cancel

Path parameters:

ParamDescription
task_idTask ID

Response:

StatusDescription
200Cancellation accepted (or best-effort cancel acknowledged)
404Task not found (including cross-account access)
409Task already in terminal state; returns current task snapshot
500Cancellation failure

JIT Token Authentication

JIT (Just-in-Time) tokens let a trusted upstream system authenticate requests on behalf of a user without pre-creating an Onlyboxes account. The Console verifies the HMAC signature, derives a deterministic account from (iss, sub), and creates that account on first use with dashboard password login disabled.

Onlyboxes has two JIT channels with separate prefixes and signing keys:

ChannelPrefixSigning keyRequired payloadAccepted byRejected by
MCP JITobx_jit_v1.CONSOLE_JIT_SIGNING_KEYiss, sub; optional exp/api/v1/commands/*, /api/v1/tasks*, /mcpDashboard management APIs
Dashboard JITobx_dashboard_jit_v1.CONSOLE_DASHBOARD_JIT_SIGNING_KEYiss, sub, scope:"dashboard"; optional expSelected management APIs, such as worker-sys provisioning/mcp and cookie-only endpoints

The two signing keys must be different. This keeps MCP execution access and dashboard automation access isolated even though they share account derivation.

Shared Account Mapping

Both channels map the same (iss, sub) pair to the same non-admin JIT account. That means an upstream identity can create a worker-sys through Dashboard JIT and later use MCP JIT under the same account ownership, without sharing signing material between the two channels.

Rules:

  • iss and sub must be non-empty strings after normalization.
  • The derived account ID is deterministic, so repeated use of the same pair reuses the same account.
  • JIT-created accounts cannot log in with a dashboard password.
  • JIT-created accounts are not admins.

MCP JIT

MCP JIT is for execution clients and MCP clients that should not require a stored trusted token.

Prerequisite: set CONSOLE_JIT_SIGNING_KEY. If unset, MCP JIT tokens are rejected.

Token format:

obx_jit_v1.<base64url-payload>.<base64url-signature>
PartContent
Prefixobx_jit_v1. — literal string, included in the signed message
PayloadBase64url (no padding) of JSON containing iss, sub, and optional exp
SignatureBase64url (no padding) of HMAC-SHA256 over obx_jit_v1.<payload>

Use as: Authorization: Bearer obx_jit_v1.<payload>.<signature>

Example payload:

{ "iss": "my-platform", "sub": "user-42", "exp": 1770000000000 }

Accepted endpoints: /api/v1/commands/*, /api/v1/tasks*, /mcp

Rejected endpoints: dashboard management APIs, including worker and token management routes.

Dashboard JIT

Dashboard JIT is for server-to-server dashboard automation, especially provisioning a user's worker-sys, without granting MCP execution access.

Prerequisite: set CONSOLE_DASHBOARD_JIT_SIGNING_KEY. It must differ from CONSOLE_JIT_SIGNING_KEY.

Token format:

obx_dashboard_jit_v1.<base64url-payload>.<base64url-signature>
PartContent
Prefixobx_dashboard_jit_v1. — literal string, included in the signed message
PayloadBase64url (no padding) of JSON containing iss, sub, scope:"dashboard", and optional exp
SignatureBase64url (no padding) of HMAC-SHA256 over obx_dashboard_jit_v1.<payload>

Example payload:

{
  "iss": "my-platform",
  "sub": "user-42",
  "scope": "dashboard",
  "exp": 1770000000000
}

Use as: Authorization: Bearer obx_dashboard_jit_v1.<payload>.<signature>

Accepted endpoints: selected management APIs that are safe for non-admin account-scoped automation, such as worker-sys provisioning and listing under the derived account.

Rejected endpoints:

  • /mcp
  • MCP/execution REST APIs
  • admin-only management APIs
  • cookie-only management APIs, including all /api/v1/tokens* endpoints

Signing Flow

  1. Pick the channel and signing key: MCP JIT or Dashboard JIT.
  2. Encode the channel-specific JSON payload as Base64url without padding.
  3. Sign <prefix><payload> with HMAC-SHA256 and the channel's key.
  4. Send the resulting token in the Authorization: Bearer ... header.
  5. The Console verifies the prefix, signature, required claims, and optional expiry.
  6. On first use, the Console derives and creates the non-admin JIT account, then handles the request under that account's ownership.

Revocation

  • Rotate CONSOLE_JIT_SIGNING_KEY to revoke all MCP JIT tokens.
  • Rotate CONSOLE_DASHBOARD_JIT_SIGNING_KEY to revoke all Dashboard JIT tokens.
  • Use short exp values on Dashboard JIT tokens when possible to reduce replay risk.

Reference Implementations

The examples below mint MCP JIT tokens without exp; add an exp Unix-milliseconds field to make them expire. To mint Dashboard JIT tokens, use the obx_dashboard_jit_v1. prefix, sign with CONSOLE_DASHBOARD_JIT_SIGNING_KEY, and include scope:"dashboard" plus optional exp in the payload:

{ "iss": "my-platform", "sub": "user-42", "scope": "dashboard", "exp": 1770000000000 }

Python

import hmac, hashlib, base64, json

def make_jit_token(signing_key: str, issuer: str, subject: str) -> str:
    payload_bytes = json.dumps({"iss": issuer, "sub": subject}, separators=(",", ":")).encode()
    payload_b64 = base64.urlsafe_b64encode(payload_bytes).rstrip(b"=").decode()
    message = f"obx_jit_v1.{payload_b64}"
    sig = hmac.new(signing_key.encode(), message.encode(), hashlib.sha256).digest()
    sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=").decode()
    return f"{message}.{sig_b64}"

token = make_jit_token("your-signing-key", "my-platform", "user-42")
# Use as:  Authorization: Bearer <token>

TypeScript / Node.js

import { createHmac } from "crypto";

function makeJitToken(signingKey: string, issuer: string, subject: string): string {
  const payload = Buffer.from(JSON.stringify({ iss: issuer, sub: subject }))
    .toString("base64url");
  const message = `obx_jit_v1.${payload}`;
  const sig = createHmac("sha256", signingKey).update(message).digest("base64url");
  return `${message}.${sig}`;
}

const token = makeJitToken("your-signing-key", "my-platform", "user-42");
// Use as:  Authorization: Bearer <token>

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
)

func makeJITToken(signingKey, issuer, subject string) (string, error) {
    payload, err := json.Marshal(map[string]string{"iss": issuer, "sub": subject})
    if err != nil {
        return "", err
    }
    payloadB64 := base64.RawURLEncoding.EncodeToString(payload)
    message := "obx_jit_v1." + payloadB64
    mac := hmac.New(sha256.New, []byte(signingKey))
    mac.Write([]byte(message))
    sigB64 := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
    return message + "." + sigB64, nil
}

// token, _ := makeJITToken("your-signing-key", "my-platform", "user-42")
// Use as:  Authorization: Bearer <token>

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public static String makeJitToken(String signingKey, String issuer, String subject) throws Exception {
    String payloadJson = String.format("{\"iss\":\"%s\",\"sub\":\"%s\"}", issuer, subject);
    String payloadB64 = Base64.getUrlEncoder().withoutPadding()
        .encodeToString(payloadJson.getBytes());
    String message = "obx_jit_v1." + payloadB64;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(signingKey.getBytes(), "HmacSHA256"));
    String sigB64 = Base64.getUrlEncoder().withoutPadding()
        .encodeToString(mac.doFinal(message.getBytes()));
    return message + "." + sigB64;
}

// String token = makeJitToken("your-signing-key", "my-platform", "user-42");
// Use as:  Authorization: Bearer <token>

See Also