api.computer.stateset.app is the production host the platform’s MCP client defaults to, and it
is still being provisioned — at the time of writing it answers 503. Everything below is the
versioned v1 contract, so you can run it today against a local instance: from the repository’s
dashboard/ directory, docker compose up -d brings the API up at http://localhost:8000 (see
the repo README). Point CUA_URL at http://localhost:8000/api/v1 and the calls are identical.How a job moves
A job is queued, a worker provisions an Xvfb desktop sandbox with a browser, the agent loops screenshot → decision → action until it is done, and the worker stops the sandbox. You never hold a connection to the sandbox — the job id is the handle for everything. Status runsqueued → running → succeeded | failed | cancelled, with a detour through awaiting_approval
whenever a policy pauses a step. GET /jobs/{job_id}/timeline shows the same lifecycle as
annotated phases: job_created, sandbox_ready, one tool_call per step, then task_completed,
task_failed or task_cancelled.
1
Confirm the key and its scopes
GET /auth/whoami echoes the auth context the key resolves to. A successful response proves
the key is active and within rate limit; scopes tells you whether the approval step later
on will be allowed. An empty list means a full-access key; an admin key can mint a narrower
one with POST /keys ({"name": "rma-receiver", "scopes": ["trigger:write", "jobs:read", "jobs:write"]}) — the plaintext comes back once in that response and never again.2
Decide which step needs a human
The console’s Mark received button is the one action in this task that changes a system of
record. An approval policy makes the job pause before a matching tool call and wait for a
reviewer. Policies are admin-scoped and match on tool-name patterns.
computer:* matches every action of the computer tool; without auto_approve_rules each one
would pause the job. The spec names allow_safe_subset and max_per_hour as supported rule
keys — their semantics are in the platform’s operator docs. Skip this step on a first run if
you prefer: the job then never enters awaiting_approval.3
Preview the job
Save the payload as The response echoes
job.json. general is the free-form built-in template: the instruction
is the whole brief — URL, record id, and the exact shape of the answer you want back.POST /jobs/preview takes that exact payload, resolves template defaults, the model alias
and agent mode, and checks quota — without creating a job or consuming an idempotency key.agent_type_kind (builtin here), resolved_model
(claude-sonnet-4-6), resolved_agent_mode (claude) and quota_ok. An agent_type_kind
of unknown means /trigger would reject the template name — preview exists so a typo
surfaces here rather than as a wasted run.4
Trigger it — with an idempotency key
Same payload, different endpoint.
Idempotency-Key (scoped to your tenant and /trigger)
makes a retry return the existing job instead of running the task twice — a replay carries
Idempotency-Replayed: true. Key it on the record; add a run counter for a deliberate rerun.5
Watch it run
Stream events. Poll.
GET /jobs/{job_id}/events is Server-Sent Events. The first frame is a
snapshot, so a late subscriber still gets one event; later frames carry status changes, the
sandbox id once provisioned, and running token and cost totals. It closes after a terminal status.: keepalive comments arrive every 15 s; after the 30-minute lifetime cap you get
event: timeout — reconnect, and the first frame is a fresh snapshot. GET /events/stream is
the same shape across every job in the tenant.Block in a shell. POST /jobs/{job_id}/wait returns as soon as the job is terminal, or
after timeout_seconds (1–60) with is_terminal: false — loop until it is true.GET /jobs/{job_id} supports conditional GET — send the previous ETag back as
If-None-Match and an unchanged job returns 304 with no body. updated_at refreshes every
iteration, so a stalled timestamp is a real signal.6
Approve the step that changes the record
When the job hits a gated action it sits in Approve and the worker resumes the job; reject and it treats the call as refused, so the agent
has to find another way or report that it was blocked. Both take an optional One other intervention exists while a job is live:
awaiting_approval until a reviewer answers. List
what is pending (pending_only=true is the default); screenshot_key is the screen the agent
was looking at when it asked.comment for the
audit trail and both need jobs:write (403 otherwise — check whoami).POST /jobs/{job_id}/extend-budget raises
max_cost_usd on a job about to trip its cap (raise-only; 409 once the job is terminal).7
Read the result and the evidence
GET /jobs/{job_id}/result is the one-call answer: status, summary, cost and artifact pointers.output_schema is enforced caller-side today: parse summary and validate it against the
schema you sent before writing the confirmation number back to your own returns record.
summary is the agent’s final text, truncated to roughly five segments of 280 characters — a
small JSON object fits, a report does not. Artifacts are the evidence: every screenshot is an
image artifact, every tool output and assistant message a text one; url is a short-lived
presigned download, storage_key the stable pointer. Four views over the same run:8
Shut it down
A finished job has nothing left to close — the worker stops the sandbox at a terminal status.
Shutting down is about jobs still live and the history you keep.Cancel sets a signal the in-flight worker sees at its next iteration checkpoint, so token spend
stops within one step.
POST /jobs/cancel-by-tag ({"tags": ["flow:rma-receive"]}) does the
same for every job carrying the tag; POST /jobs/cleanup deletes terminal jobs older than
older_than_days (minimum 7). Both default to dry_run: true and neither touches a live job.The same task through the MCP server
The hostedstateset-computer-use MCP server is a thin client for exactly these endpoints, so an
agent in Claude Desktop, Claude Code or your own host runs the task without writing HTTP. Configure
it with STATESET_CUA_API_URL and STATESET_CUA_API_KEY as on the MCP servers page:
A host prompt that drives it: “Use stateset-computer-use. Preview first, then trigger with
idempotency_key
rma-receive-48213, max_cost_usd 2, tags flow:rma-receive and rma:48213, and
the instruction in job.json. Wait for the result with timeout_ms 600000 and give me
confirmation_number from the summary.”
The hosted MCP server has no approval tool. If a policy pauses the job,
wait_for_result keeps
polling through awaiting_approval; a person approves in the dashboard or with
POST /approvals/{approval_id}/approve as above. Keep timeout_ms generous when a gate is in play.When it does not go to plan
status is cancelled and nobody cancelled it
status is cancelled and nobody cancelled it
The cost cap tripped: the worker cancels before the next model call once
max_cost_usd is
crossed, and error says so. Read /replay to see how far it got; raise the cap next run.The summary is prose, not the JSON asked for
The summary is prose, not the JSON asked for
The agent hit something it could not resolve — a login wall, a record it could not find. The
status enum in the schema gives it a legitimate way to say not_found or blocked; the
transcript has the reason.What you built
Next steps
Make it repeatable
POST /jobs/{id}/clone-to-template turns this run into a named template you trigger by agent_type.Let the platform call you
Register a tenant webhook for
job.completed and job.failed instead of polling — and verify it per the webhook security guide.Full v1 reference
Every endpoint used here, generated from the platform’s own OpenAPI.