This document is the unified reference for all public APIs exposed by Onlyboxes.
- Console HTTP base URL:
http://<console-host>:8089 - Console worker gRPC endpoint:
<console-host>:50051 - API version prefix:
/api/v1
Onlyboxes has two auth paths:
- Dashboard session (cookie): for web/admin APIs
- Access token (Bearer): for execution APIs and MCP
- Cookie name:
onlyboxes_console_session - Created by:
POST /api/v1/console/login - Used by:
/api/v1/console/session/api/v1/console/logout/api/v1/console/password/api/v1/console/register/api/v1/console/accounts*/api/v1/console/tokens*/api/v1/workers*(role-scoped worker routes)
- Session TTL is 12 hours in-memory; console restart invalidates all sessions.
- Header format:
Authorization: Bearer <access-token> - Used by:
/api/v1/commands/*/api/v1/tasks*/mcp
- If no token exists in console, token-protected APIs return
401.
- Content type:
application/json - Error body (most APIs):
{ "error": "message" }- Time fields are RFC3339 timestamps.
- IDs are opaque strings (for example
acc_*,tok_*, worker UUIDs, task IDs).
POST /api/v1/console/login
Request:
{
"username": "admin",
"password": "secret"
}Success 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://..."
}Errors:
400invalid JSON body401invalid username/password500session creation failure
GET /api/v1/console/session
- Requires dashboard cookie auth.
- Success body format is same as login response.
Errors:
401not authenticated
POST /api/v1/console/logout
- Clears cookie and removes in-memory session if present.
Response:
204 No Content
POST /api/v1/console/register
Request:
{
"username": "dev-user",
"password": "strong-password"
}Success 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"
}Validation and errors:
403registration disabled (CONSOLE_ENABLE_REGISTRATION=false)403caller is not admin400username empty, username length > 64, or password empty409username already exists (case-insensitive)500database/internal failure
POST /api/v1/console/password
Request:
{
"current_password": "old-password",
"new_password": "new-password"
}Responses:
204password updated400invalid JSON body, missingcurrent_password, or missingnew_password401current password is incorrect500internal failure
Notes:
- This endpoint requires dashboard session auth.
- Password update rotates active sessions for the account.
GET /api/v1/console/accounts?page=1&page_size=20
Query:
page: positive integer, default1page_size: positive integer, default20, max100
Success 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
}Errors:
400invalid query values403caller is not admin500database/internal failure
DELETE /api/v1/console/accounts/:account_id
Responses:
204deleted403deleting current account is forbidden403deleting admin account is forbidden404account not found500internal failure
Tokens are account-scoped. A user can manage only their own tokens.
GET /api/v1/console/tokens
Success 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
}POST /api/v1/console/tokens
Request:
{
"name": "ci-prod",
"token": "optional-manual-token"
}namerequired, trimmed, length <= 64, unique per account (case-insensitive)tokenoptional:- omitted => auto-generate (
obx_<hex>) - provided => required non-empty after trim, no whitespace, max length 256
- omitted => auto-generate (
Success 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"
}Errors:
400validation error409token name conflict or token value conflict500internal failure
DELETE /api/v1/console/tokens/:token_id
Responses:
204deleted404token not found (or not owned by current account)
GET /api/v1/console/tokens/:token_id/value
Always returns 410 Gone:
{
"error": "token value is only returned at creation time; delete and recreate the token to obtain a new value"
}Worker types:
normal(maps toworker-docker)worker-sys(maps toworker-sys)
Permission matrix:
- admin:
- list/stats/inflight: all workers
- delete: any worker
- create:
normalandworker-sys
- non-admin:
- list/stats/inflight: only own
worker-sys - delete: only own
worker-sys(other targets return404) - create: only
worker-sys, max one per account
- list/stats/inflight: only own
GET /api/v1/workers?page=1&page_size=20&status=all
Query:
page: positive integer, default1page_size: positive integer, default20, max100status:all|online|offline, defaultall
Success 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
}Errors:
400invalid query values
GET /api/v1/workers/stats?stale_after_sec=30
Query:
stale_after_sec: positive integer, default30
Success 200:
{
"total": 5,
"online": 4,
"offline": 1,
"stale": 1,
"stale_after_sec": 30,
"generated_at": "2026-02-21T00:00:00Z"
}Note: non-admin responses are scoped to the caller-owned worker-sys.
GET /api/v1/workers/inflight
Success 200:
{
"workers": [
{
"node_id": "worker-1",
"capabilities": [
{ "name": "pythonExec", "inflight": 1, "max_inflight": 4 }
]
}
],
"generated_at": "2026-02-21T00:00:00Z"
}Note: non-admin responses are scoped to the caller-owned worker-sys.
POST /api/v1/workers
Request body:
{
"type": "normal"
}Rules:
typeis required, value must benormal|worker-sys.- only admin can create
normal. - every account can create at most one
worker-sys.
Success 201:
{
"node_id": "2f51f8f9-77f2-4c1a-a4f5-2036fc9fcb9e",
"type": "normal",
"command": "WORKER_CONSOLE_GRPC_TARGET=127.0.0.1:50051 WORKER_ID=... WORKER_SECRET=... WORKER_HEARTBEAT_INTERVAL_SEC=5 WORKER_HEARTBEAT_JITTER_PCT=20 ./path-to-binary"
}Notes:
WORKER_SECRETappears only here (one-time return)../path-to-binaryis a placeholder and must be replaced by your real worker executable command.
Errors:
400invalid request body / invalidtype403non-admin creatingnormal409caller already owns aworker-sys503provisioning unavailable500create failure
DELETE /api/v1/workers/:node_id
Responses:
204deleted404worker not found (also returned for unauthorized non-admin targets)400missingnode_id503provisioning unavailable
GET /api/v1/workers/:node_id/startup-command
Always returns 410 Gone:
{
"error": "worker secret is returned only when creating the worker; delete and recreate to get a new startup command"
}POST /api/v1/commands/echo
Request:
{
"message": "hello",
"timeout_ms": 5000
}Rules:
message: required, non-empty after trimtimeout_ms: optional, range1..60000, default5000
Success 200:
{ "message": "hello" }Errors:
400invalid body / missing message / timeout out of range429no worker capacity503no online worker supports echo504timeout502execution/internal error
POST /api/v1/commands/terminal
Request:
{
"command": "pwd",
"session_id": "optional-session",
"create_if_missing": false,
"lease_ttl_sec": 60,
"timeout_ms": 60000,
"request_id": "optional-idempotency-key"
}Rules:
command: required, non-emptytimeout_ms: optional, range1..600000, default60000request_id: optional, idempotency key scoped per account
Success 200:
{
"session_id": "sess_xxx",
"created": true,
"stdout": "...",
"stderr": "...",
"exit_code": 0,
"stdout_truncated": false,
"stderr_truncated": false,
"lease_expires_unix_ms": 1770000000000
}Errors:
400invalid body/params orinvalid_payload404session_not_found409session_busyor canceled429no worker capacity503no compatible worker504timeout502unexpected execution failure
POST /api/v1/commands/computer-use
Request:
{
"command": "pwd",
"timeout_ms": 60000,
"request_id": "optional-idempotency-key"
}Rules:
command: required, non-emptytimeout_ms: optional, range1..600000, default60000request_id: optional, idempotency key scoped per accountlease_ttl_secis ignored if provided by legacy clients- routing is account-scoped: requests are dispatched only to caller-owned
worker-sys - account-scoped concurrency is single-flight (
max_inflight=1)
Success 200:
{
"stdout": "...",
"stderr": "...",
"exit_code": 0,
"stdout_truncated": false,
"stderr_truncated": false
}Errors:
400invalid body/params orinvalid_payload409workersession_busyor task canceled429no worker capacity (no_capacity)503no caller-owned onlineworker-sys(no_worker)504timeout502unexpected execution failure
Task ownership is account-scoped by token.
POST /api/v1/tasks
Request:
{
"capability": "pythonExec",
"input": { "code": "print(1)" },
"mode": "auto",
"wait_ms": 1500,
"timeout_ms": 60000,
"request_id": "optional-idempotency-key"
}Rules:
capability: required, non-emptyinput: must be valid JSON (defaults to{}when omitted)mode:sync|async|auto, defaultautowait_ms:1..60000, default1500timeout_ms:1..600000, default60000request_id: optional dedupe key (scoped per account)
Possible responses:
202task still running (containsstatus_url)200completed succeeded409completed canceled504completed timeout429completed failed witherror.code=no_capacity503completed failed witherror.code=no_worker502completed failed (other error codes)
202 example:
{
"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"
}Completed example:
{
"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"
}Error body inside task payload:
"error": {
"code": "execution_failed",
"message": "..."
}Submit-time errors:
400invalid request/mode/wait/timeout/body409request_id already in progress429no worker capacity503no compatible worker504deadline exceeded502submit failure
GET /api/v1/tasks/:task_id
Responses:
200task snapshot404task not found (including cross-account access)
POST /api/v1/tasks/:task_id/cancel
Responses:
200canceled (or best-effort cancel accepted)404task not found (including cross-account access)409task already terminal (returns task snapshot)500cancel failure
Endpoint: POST /mcp
- Transport: MCP Streamable HTTP
- Server mode: stateless JSON response
GET /mcpreturns405withAllow: POST- Requires
Authorization: Bearer <access-token> - Recommended headers:
Content-Type: application/jsonAccept: application/json, text/event-stream
Supported MCP flow includes standard methods such as:
initializetools/listtools/call
Tool argument schemas use additionalProperties=false.
Unknown arguments are rejected with JSON-RPC -32602 invalid params.
Tools listed in
CONSOLE_HIDDEN_TOOLSare omitted fromtools/list. They remain callable viatools/callif the client already knows the tool name.
Input:
{ "message": "hello", "timeout_ms": 5000 }messagerequiredtimeout_msoptional,1..60000, default5000
Output:
{ "message": "hello" }Input:
{ "code": "print(1)", "timeout_ms": 60000 }coderequiredtimeout_msoptional,1..600000, default60000
Output:
{ "output": "1\n", "stderr": "", "exit_code": 0 }Note: non-zero exit_code is returned as normal tool output.
Input:
{
"command": "pwd",
"session_id": "optional",
"create_if_missing": false,
"lease_ttl_sec": 60,
"timeout_ms": 60000
}commandrequiredsession_idoptionalcreate_if_missingoptional, defaultfalselease_ttl_secoptionaltimeout_msoptional,1..600000, default60000
Output:
{
"session_id": "sess_xxx",
"created": true,
"stdout": "...",
"stderr": "...",
"exit_code": 0,
"stdout_truncated": false,
"stderr_truncated": false,
"lease_expires_unix_ms": 1770000000000
}Input:
{
"command": "pwd",
"timeout_ms": 60000,
"request_id": "optional-idempotency-key"
}commandrequiredtimeout_msoptional,1..600000, default60000request_idoptional, idempotency key scoped per account- routed only to caller-owned
worker-sys - no terminal session fields (
session_id,create_if_missing,created)
Output:
{
"stdout": "...",
"stderr": "...",
"exit_code": 0,
"stdout_truncated": false,
"stderr_truncated": false
}Input:
{ "session_id": "sess_xxx", "file_path": "/workspace/a.png", "timeout_ms": 60000 }session_idrequiredfile_pathrequiredtimeout_msoptional,1..600000, default60000- when
session_idis exactlycomputerUse, routing uses caller-ownedworker-sysreadImagecapability - for other
session_idvalues, routing usesterminalResourcecapability
Behavior:
- If target MIME is
image/*: returns one image content item. - If non-image MIME: returns one text content item:
unsupported mime type: <mime>; expected image/*
- Missing/invalid token: HTTP
401 - Invalid tool params: JSON-RPC error
-32602 - Execution failures: returned as MCP tool error content (
isError=true)
Service:
service WorkerRegistryService {
rpc Connect(stream ConnectRequest) returns (stream ConnectResponse);
}Worker establishes a bidirectional stream and typically sends:
ConnectRequest.hello(ConnectHello)- Periodic
ConnectRequest.heartbeat(HeartbeatFrame) ConnectRequest.command_result(CommandResult) for dispatched commands
Console responds with:
ConnectResponse.connect_ack(ConnectAck)ConnectResponse.heartbeat_ack(HeartbeatAck)ConnectResponse.command_dispatch(CommandDispatch)
ConnectHelloincludes worker identity, capabilities, labels, version, andworker_secret.CommandDispatchcarries:command_idcapabilitypayload_jsondeadline_unix_ms
CommandResultcarries:command_id- optional
error { code, message } payload_jsoncompleted_unix_ms
- Console gRPC has no built-in TLS/mTLS in this release.
worker-dockerrejects insecure console endpoints by default, and allows plaintext only whenWORKER_CONSOLE_INSECURE=true.worker-sysexecutescomputerUsedirectly on host shell (/bin/sh -lc) without container isolation.worker-sysreadImagereads host files directly and accepts onlysession_id=computerUse.- deploy
worker-sysonly on dedicated hosts with strict OS-level access controls. - Put console HTTP (
:8089) and gRPC (:50051) behind a reverse proxy/gateway and enforce TLS for external access. - Keep gRPC endpoint private and tunnel/encrypt traffic in production.
- Token plaintext and
WORKER_SECRETare one-time return values. GET /api/v1/console/tokens/:token_id/valueandGET /api/v1/workers/:node_id/startup-commandare intentionally410 Gone.