# Provision X — Agent API v1 Agents with a computer built in, over one API. Create an agent and it wakes up on its own isolated machine, already running: full Linux desktop, Chrome, shell, persistent disk — everything preinstalled, powered by OpenClaw. Computer use is native, not a tool the agent calls out to: browsing, files, and the desktop are the agent's own hands on its own machine. Send it work in one call, poll or stream the result, watch its screen live; it suspends itself when idle and wakes on the next request. This file is the complete API reference in one document — point a coding agent at it and it can scaffold a working client. Base URL: {BASE_URL}/api/v1 Local dev: http://provision-x.test/api/v1 ## Authentication Every request sends a bearer key: Authorization: Bearer pvx_live_... Sign up at https://x.provision.ai/signup (you get a team, a $5 wallet credit and your first key), then create and revoke keys at https://x.provision.ai/console/keys. A key is shown once and acts for its team only: every agent, run, session and usage record you can see belongs to your team. Requests are rate-limited to 120/minute per key. ## Billing: team wallet, caps, pause LLM usage by your agents is metered per call and paid from your team's prepaid wallet (USD). Fund it and set auto-reload in the console (https://x.provision.ai/console/billing); the API is read-only for money. - `GET /wallet` → `{ balance_usd, auto_reload: { enabled, threshold_usd, amount_usd, last_error }, transactions: [...] }` - `GET /usage?after=` → your team's metered calls, oldest first (`next_cursor`, `has_more`); events settle ~2s before they appear. Each event: `agent_id, model, upstream_model, prompt_tokens, completion_tokens, cost_usd`. - `GET /agents/{id}/usage` → that agent's spend, cap and events. - `PATCH /agents/{id}` with `spend_cap_usd` (per-agent ceiling on wallet spend; default 100, null = none) and/or `llm_paused` (true/false — your own switch, e.g. when your end user is out of credit). - When an agent is paused, at its cap, or the wallet is empty, it does not error: its model replies explaining why and the gateway sets `X-Provision-Llm-Block: agent_paused | agent_cap_reached | wallet_empty`. It resumes the moment the condition clears. - Webhooks (to the agent's `webhook_url`): `agent.spend_cap_reached`, `team.wallet_empty`. - Free teams (no card, never paid) can run 1 agent; creating more returns `402 payment_required` until you add funds. ## Conventions - Ids are prefixed ULIDs: `agt_` agent, `ses_` session, `run_` run, `msg_` message, `sub_` sub-agent. - Timestamps are ISO 8601 strings. - Lists wrap results as `{ "data": [...] }`, newest first. - Errors always use one envelope: { "error": { "type": "...", "message": "...", "details": { ... }? } } Types: `unauthorized` (401), `not_found` (404), `validation_error` (422, with per-field `details`), `rate_limited` (429), `agent_unavailable` (409), `subagent_deleted` (409), `subagent_busy` (409), `subagent_not_found` (404), `agent_outdated` (409), `subagent_limit` (409), `roster_busy` (409), `slug_unavailable` (409). - Mutating requests accept an `Idempotency-Key` header; retrying with the same key returns the original resource instead of creating a duplicate. ## Resource model - **Agent** — a persistent, isolated virtual machine running the agent. Create one per end user. Statuses: `provisioning`, `running`, `suspended` (machine stopped; wakes transparently on the next run), `error`, `deleted`. - **Session** — one conversation on an agent. Holds full history on the machine; you only ever send the new input. An agent holds many. - **Run** — one agentic turn: your input, the agent's work, its reply. Statuses: `queued`, `running`, `completed`, `needs_input` (the agent stopped to ask you something — see Run outcomes), `failed`, `cancelled`. - **Sub-agent** — an extra persona on the same machine with its own workspace, instructions, and optional model. Sessions bind to one via `subagent_id`; omitted means the default agent. A persona boundary, not a security boundary. ## Agents ### Create an agent POST /v1/agents { "name": "Riley", // required "instructions": "You are ...", // persona, optional "model": "smart", // alias: smart | fast | cheap "external_id": "user_42", // your attribution tag, echoed back "webhook_url": "https://...", // run lifecycle webhooks, optional "metadata": { ... }, // yours, never interpreted "auto_sleep": true, // default true "idle_timeout_seconds": 1800, // 300–86400, default 1800 "llm_key": "sk-...", // per-agent LLM credential, // write-only (never returned); // platform default when omitted "llm_base_url": "https://..." // per-agent OpenAI-compatible // endpoint; platform default // when omitted } Returns 202 with the agent in `status: "provisioning"`. A dedicated VM is forked from the platform image and configured; poll `GET /v1/agents/{id}` until `status: "running"` (~2 minutes). `agent.ready` fires on the webhook. With `auto_sleep`, an agent idle past `idle_timeout_seconds` is suspended: the machine stops (state kept on disk) and any new run wakes it transparently, adding ~30–60s to that run. ### List agents GET /v1/agents?external_id=...&status=...&limit=25 ### Get an agent GET /v1/agents/{id} Status reflects live machine state. Fields include `substrate` (which machine platform the agent runs on), `auto_sleep`, `idle_timeout_seconds`, `last_activity_at`, `ready_at`, `last_error`. ### Delete an agent DELETE /v1/agents/{id} Destroys the machine and everything on it. Irreversible. ### Desktop link POST /v1/agents/{id}/desktop_links Returns a live view of the agent's actual desktop. Wakes the machine if suspended. Body (optional): {"origin": "https://app.example.com"} — your web app's exact browser origin. When given, the websocket grant is bound to that origin and the page can connect a viewer (e.g. noVNC) to `ws_url?token=` directly. The origin must be registered with the machine platform first (mola: Settings → Desktop origins; HTTPS origins, plus http://localhost / http://127.0.0.1 with optional port for development). Without an origin the grant only accepts no-Origin clients (native apps, server-side relays) — browsers are refused. { "mode": "view", "url": "https://...", // browser viewer; on some machine // platforms it requires operator // access rather than a tokenized link "expires_at": null, "websocket": { // when present: the customer-facing "ws_url": "wss://...", // path — an embeddable desktop stream "token": "...", // grant, single-use, connect before "expires_at": "..." // expires_at (~60s) } } For end-user viewing, embed a viewer against the `websocket` grant and mint a fresh link each time the view opens. Never cache the token or embed it in shipped frontend code. ## Runs — the one-call turn API ### Start a run POST /v1/agents/{id}/runs { "input": "Research the top 3 EV makers, write a memo.", // required "session_id": "ses_...", // continue a thread; omit to mint one "subagent_id": "sub_...", // only when minting; routes the new // session to that sub-agent "metadata": { ... }, // echoed on the run "stream": false // true: respond with SSE (see below) } Returns 202 with the run; `session_id` on it is the session to reuse for the next turn. Wakes a suspended agent automatically. 409 `agent_unavailable` while the agent is provisioning or errored. ### Get a run GET /v1/runs/{id} { "id": "run_...", "object": "run", "session_id": "ses_...", "status": "completed", "output_text": "Here is the memo...", // full reply, untruncated; // null until terminal "input_request": null, // set on needs_input runs: // { "type": "question", // "message": "..." } "result": { "summary": "...", "message_ids": [...], "outcome": { ... }? }, // see Run outcomes "error": null, "metadata": { ... }, "started_at": "...", "ended_at": "...", "created_at": "..." } ### Run outcomes When outcome classification is enabled on the platform, every finished run is classified from the agent's final reply: - A run where the agent stopped to ask you something (a clarifying question, missing credentials, an approval) finishes as `status: "needs_input"` with `input_request.message` carrying the agent's question. Answer by sending the next input on the **same session** — the conversation picks up where it stopped. - All other runs finish `completed` (or `failed` on execution errors), with the raw classification attached at `result.outcome`: { "outcome": "completed", // completed | needs_input | // refused | failed "confidence": 0.97, "probabilities": { "completed": 0.97, "needs_input": 0.01, ... }, "accomplished": 0.94 } // P(task fully accomplished) When classification is disabled or unavailable, runs finish `completed` exactly as before and `result.outcome` is absent. ### Stream a run (SSE) GET /v1/runs/{id}/stream — or pass `stream: true` on create. Server-Sent Events until the run is terminal: event: run.created data: {"id":"run_...","session_id":"ses_...","status":"queued"} event: run.status data: {"id":"run_...","status":"running"} event: run.completed data: {"id":"run_...","session_id":"ses_...","status":"completed", "output_text":"...","error":null} Terminal events are `run.completed`, `run.needs_input`, `run.failed`, `run.cancelled` (each carries `output_text` and `input_request`); after ~11 minutes without one the stream ends with `run.timeout` (the run itself keeps executing — re-attach or poll). This is status-level streaming, not token streaming. Use `curl -N` to watch live. ### Cancel a run POST /v1/runs/{id}/cancel ## Sessions Sessions never expire and are never auto-reset; they live until deleted. POST /v1/agents/{id}/sessions { "title"?, "external_id"?, "subagent_id"?, "metadata"? } GET /v1/agents/{id}/sessions GET /v1/sessions/{id} includes recent messages DELETE /v1/sessions/{id} GET /v1/sessions/{id}/messages full history, oldest first POST /v1/sessions/{id}/messages { "content": "..." } → { message, run } — session-first equivalent of POST /agents/{id}/runs ## Sub-agents Extra roster members on the agent's machine. Creating or deleting one is hot: no restart, in-flight runs untouched. Max 10 per agent. POST /v1/agents/{id}/subagents { "name": "Scout", "instructions"?, "model"?, "browser_profile"? } → 201, synchronous (~5s) GET /v1/agents/{id}/subagents GET /v1/agents/{id}/subagents/{sid} DELETE /v1/agents/{id}/subagents/{sid} 409 subagent_busy while it has queued/running work `browser_profile` controls how the sub-agent browses: - `"own"` (default) — a dedicated browser profile (separate cookies, logins, tabs), so personas never clobber each other's browser work. - `"shared"` — the machine's default profile, shared with the main agent and other shared sub-agents: same logins, cookies, and connected integrations, for personas that collaborate on the same accounts. Bind a session with `subagent_id` (on session create, or on the one-call run when minting). The default agent can delegate to sub-agents on its own (chief-of-staff wiring is automatic). Deleting a sub-agent prunes its workspace on the machine; API-side history is kept, and further messages to its sessions return 409 `subagent_deleted`. Agents created before sub-agent support return 409 `agent_outdated` — recreate them. ## Memory Shared notes delivered to every workspace on the agent's machine as `TEAM.md` (the main agent and all sub-agents see the same file; personas are instructed to read it before starting work). Synced live without a restart; survives re-provisioning. PUT /v1/agents/{id}/memory { "content": "..." | null } GET /v1/agents/{id}/memory → { content, updated_at } Null or empty content removes the file from every workspace. ## Secrets Write-only key/value pairs delivered to the agent's workspace as `.secrets.env`, synced live without a restart. PUT /v1/agents/{id}/secrets { "secrets": { "NAME": "value" } } GET /v1/agents/{id}/secrets names + set_at only, never values DELETE /v1/agents/{id}/secrets/{name} ## Webhooks Set `webhook_url` on the agent. Events: `agent.ready`, `run.completed`, `run.needs_input`, `run.failed`. Each POST carries the JSON payload plus: X-Signature: t=,v1= `v1` is HMAC-SHA256 of `"{t}.{raw_body}"` with your webhook signing secret. Reject if `t` is older than 5 minutes or the signature mismatches. Deliveries retry on failure with backoff (10s, 60s, 5m, 30m). ## Models `model` on the agent is an alias — `smart` (default), `fast`, `cheap` — mapped server-side to concrete models (currently gpt-4.1 / gpt-4.1-mini / gpt-4.1-nano); sub-agents accept the same aliases and override the machine default per sub-agent. LLM routing and billing are platform-managed by default. To bring your own routing, set `llm_key` (and optionally `llm_base_url`, any OpenAI-compatible endpoint) when creating the agent: the credential is stored encrypted, pasted into the machine's auth store at provision time, and never returned by the API. The credential is machine-global — the agent and all its sub-agents share it. ## Typical flow # 1. Create an agent (~2 min to running) curl -X POST $BASE/agents -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Riley","instructions":"Concise research assistant."}' # 2. Poll until running curl $BASE/agents/agt_... -H "Authorization: Bearer $KEY" # 3. One-call run (mints a session) curl -X POST $BASE/agents/agt_.../runs -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{"input":"Find the top 3 EV makers, write a memo."}' # 4. Stream it curl -N $BASE/runs/run_.../stream -H "Authorization: Bearer $KEY" # 5. Continue the thread curl -X POST $BASE/agents/agt_.../runs -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{"input":"Make it shorter.","session_id":"ses_..."}' # 6. Watch the agent work curl -X POST $BASE/agents/agt_.../desktop_links -H "Authorization: Bearer $KEY"