/ docsREST APIllms.txt
On this page
AuthenticationRunsDatasetsFilesBuildsScenarios & plansSchedulesTargets & discoveryDevicesWorkspace

REST API

The machine-reachable surface — exactly the endpoints the CLI and the MCP server drive, and nothing the allowlist does not deliberately open.

Authentication

Send the workspace API key as a bearer token: Authorization: Bearer gp_…. The key is WORKSPACE-scoped — it carries no user identity — and it is judged on every request: unknown and revoked keys get one uniform 401, and a valid key whose workspace plan does not include API access is refused the same way (the entitlement is re-checked at use, so a plan change takes effect on the next request, and a key minted before the plan carries API access starts working the moment it does).

A 401 has four meanings: no key, an invalid key, a revoked key — or a valid key on a plan without API access. The response does not distinguish them (deliberately, for the first three); if a working pipeline starts getting 401s after a plan change, it is the fourth.

Machine callers are default-denied: a key reaches only the endpoints below. Everything else — members, billing writes, key minting itself — answers 403 to a key, however valid.

Runs

EndpointDoes
POST /api/workspaces/{workspaceId}/runsTrigger a run (scenarioId, optional model / deviceSpecJson / mode / planVersion; optional datasetId + datasetRow bind one row of a dataset to the run) — the gates run BEFORE anything spends.
GET /api/workspaces/{workspaceId}/runsList runs, newest first (paged; filter by scenario, status, failedOnly).
GET /api/workspaces/{workspaceId}/runs/{runId}One run's summary: status, timing, cost, model.
POST /api/workspaces/{workspaceId}/runs/{runId}/cancelStop a run — queued or executing. The record goes terminal at once; the device finishes the step it is already on before it stops. Not reversible. Stored as Error with failureClass UserCancelled — the pair, not the status alone, is how a stop reads apart from a break. A run that already finished answers 409, which is a benign race and not a caller error.
POST /api/workspaces/{workspaceId}/runs/{runId}/mirror-ticketA short-lived ticket to watch the run's device live: GET api/workspaces/{workspaceId}/runs/{runId}/mirror?ticket=… opens an MJPEG stream an img element can show, if it arrives before expiresAt. Bound to this run and this workspace — a stream and nothing else. 404 for another workspace's run, an unknown one, or a device that cannot stream.
POST /api/workspaces/{workspaceId}/runs/{runId}/last-picture-ticketA short-lived ticket for a finished run's last picture, the screen after the last step that kept one: GET api/workspaces/{workspaceId}/runs/{runId}/last-picture?ticket=… answers it as a PNG or JPEG an img element can show, from this API, once, if it arrives before expiresAt. step says which step it follows. Bound to this run and this workspace — a picture and nothing else. 404 for another workspace's run, an unknown one, or a run that kept no picture; 409 while the run is still going.
GET /api/workspaces/{workspaceId}/runs/{runId}/detailThe run with its ordered per-step results and the files it kept (name, size, type, hash, state: kept / not-kept / swept), and the files it SENT into the app (name, size, type, the hash of the bytes that went, and the page they went to).
GET /api/workspaces/{workspaceId}/runs/{runId}/files/{fileId}The bytes of a file this run kept — a download the page handed the browser — streamed under its own name; 410 with the record's own sentence once the retention sweep erased them, or when the file was never kept (over a ceiling).
GET /api/workspaces/{workspaceId}/runs/{runId}/logsDevice/server logs, redacted server-side.
GET /api/workspaces/{workspaceId}/runs/{runId}/networkCaptured network entries, masked server-side (credentials by name in addresses, headers and bodies, and saved secrets), or why there are none.

Datasets

EndpointDoes
POST /api/workspaces/{workspaceId}/datasetsCreate a dataset from CSV text (name, csv): the first line names the columns, every later line is a row a run can be bound to. Refused at the write when it is too big — the sentence names the number and the key.
GET /api/workspaces/{workspaceId}/datasetsList the workspace's datasets, newest first (paged): columns and row counts, never the cells.No screen in the app — not yet built — the app has no datasets screen, and the CLI and MCP have no dataset verb; a workspace's sets are reachable through this API alone, which is why the row is here and says so.
GET /api/workspaces/{workspaceId}/datasets/{datasetId}One dataset: its columns, row count, last use and the retention expiry — a deleted one reads as a tombstone with deletedAt and deletedBy.No screen in the app — not yet built — the app has no datasets screen, and the CLI and MCP have no dataset verb; the run detail's datasetId is the only place the app hands out this id.
DELETE /api/workspaces/{workspaceId}/datasets/{datasetId}Delete a dataset: the cells are erased at once; a tombstone keeps the name, columns and row count so every run that used a row of it still says which. Idempotent.

Files

EndpointDoes
GET /api/workspaces/{workspaceId}/files/limitsThe ceilings an upload is judged by, as the AI Testing service is configured now: the most a file may be, the most an upload request may be, how many files a workspace keeps, and the longest name a file may have.
POST /api/workspaces/{workspaceId}/filesUpload one file (multipart/form-data, exactly one file part) so a scenario can send it — a step names it as {{file:NAME}}. Refused over the ceiling, before the body is read when the request declares its length; refused for a name that is a path, carries an invisible character, or is already kept. The sentence says which.
GET /api/workspaces/{workspaceId}/filesList the workspace's files, newest first (paged): name, size, the type the upload declared, the reference a step writes, and the scenarios whose steps name each one — never the bytes.
GET /api/workspaces/{workspaceId}/files/{fileId}One file's facts and the scenarios that name it — a deleted one reads as a tombstone with deletedAt.
DELETE /api/workspaces/{workspaceId}/files/{fileId}Delete a file: its bytes are erased first, then a tombstone keeps the name, size and dates, so a run whose scenario still names it stops at that step with a sentence saying the file was deleted. The answer lists those scenarios. Idempotent.

Builds

EndpointDoes
GET /api/workspaces/{workspaceId}/builds/limitsWhat a build upload is judged by, as the AI Testing service is configured now: whether it keeps builds at all, the most a build may be, and what each platform accepts.
POST /api/workspaces/{workspaceId}/buildsUpload one mobile app build (multipart/form-data, exactly one file part, ?platform=Android or iOS): an .apk, an .ipa, or a .zip holding one simulator .app. The build is read before anything is kept, and the answer is its id with the app id and version its manifest names — a create or an edit then names it as buildId. Refused over the ceiling, before the body is read when the request declares its length; refused in the AI Testing service's own words for a file that is not the build its name says, an unsafe archive, or a manifest with no app id. A build no app takes is removed within a day.
GET /api/workspaces/{workspaceId}/targets/{targetId}/buildsAn app's uploaded builds, newest first, and which one it runs: name, size, the app id and version read from each file, and when it was uploaded — never where a build is kept.
PUT /api/workspaces/{workspaceId}/targets/{targetId}/builds/{buildId}/currentMake an uploaded build the one the app runs ("Use this build"). Refused for a build of a different app id, of the other platform, of another app, or already removed — the sentence says which. With a key, answers with the app's id, name, platform and package or bundle id, the build it now runs, and where that build installs.

Scenarios & plans

EndpointDoes
POST /api/workspaces/{workspaceId}/scenariosCreate a scenario (a draft).
GET /api/workspaces/{workspaceId}/scenariosList scenarios (paged lookup table).
GET /api/workspaces/{workspaceId}/scenarios/{scenarioId}One scenario, full definition.
PUT /api/workspaces/{workspaceId}/scenarios/{scenarioId}Update name / description / steps.
POST /api/workspaces/{workspaceId}/scenarios/{scenarioId}/compileCompile the steps into the deterministic plan runs replay (a spend; floors at 1 credit).
GET /api/workspaces/{workspaceId}/scenarios/{scenarioId}/compile-run/{compileRunId}The authoritative outcome of a compile.
GET /api/workspaces/{workspaceId}/scenarios/{scenarioId}/plan/versionsEvery compiled plan version, newest first.
PUT /api/workspaces/{workspaceId}/scenarios/{scenarioId}/plan/versionPin (or clear) which plan version unnamed runs replay.
POST /api/workspaces/{workspaceId}/scenarios/{scenarioId}/steps/{order}/unpin-selectorTake a pin off one step — body { selector }: the selector (or drag drop end) that scenario step carries now, which is checked before anything changes. The scenario goes back to Draft.
GET /api/workspaces/{workspaceId}/scenarios/{scenarioId}/baselinesEvery picture a person approved as the scenario's visual baselines — per step, one per screen, with who approved it and through which door, the run it came from and that run's device, and the picture at an address that opens for minutes.
DELETE /api/workspaces/{workspaceId}/scenarios/{scenarioId}/baselines/{baselineId}Delete one approved baseline. It cannot be undone: the next run of that step on that screen asks for its picture to be approved again. Approving a picture stays a person's, on the run's page in the app.
GET /api/workspaces/{workspaceId}/scenarios/{scenarioId}/plan/decisionsWhat the compile pipeline decided, per step.No screen in the app — not yet built in the app — the per-step audit trail is read through the CLI (nth scenarios decisions) and the MCP tool get_plan_decisions; the app shows a scenario's plan and its versions, not the decisions behind each step.

Schedules

EndpointDoes
GET /api/workspaces/{workspaceId}/schedulesList schedules (paged).
GET /api/workspaces/{workspaceId}/schedules/preview-occurrencesThe next N fire times for a cron + IANA timezone, computed server-side.
GET /api/workspaces/{workspaceId}/schedules/estimateWhat a schedule would cost — per run, per cycle, per month — BEFORE creating it.
GET /api/workspaces/{workspaceId}/schedules/{scheduleId}One schedule: cron, timezone, enabled/paused state, next fire.
POST /api/workspaces/{workspaceId}/schedulesCreate a schedule.
PUT /api/workspaces/{workspaceId}/schedules/{scheduleId}Update a schedule — a full replace, not a patch.
POST /api/workspaces/{workspaceId}/schedules/{scheduleId}/pauseStop future cron firings (idempotent). A paused schedule is not inert: run-now still fires it, deliberately.
POST /api/workspaces/{workspaceId}/schedules/{scheduleId}/resumeRe-enable — the next run recomputes from now; missed cycles are never fired retroactively.
POST /api/workspaces/{workspaceId}/schedules/{scheduleId}/run-nowFire one Manual cycle — a SPEND through the cron's own billing gates; works on a paused schedule; every call is a new billed cycle. The cycle records who fired it.

Targets & discovery

EndpointDoes
GET /api/workspaces/{workspaceId}/targetsList the workspace's apps (paged): with a key, each app's id, name, platform, package or bundle id, whether its ownership is confirmed, and the build it runs.
POST /api/workspaces/{workspaceId}/targetsRegister an app (requires the ownership attestation; refuses over the plan's app limit).
GET /api/workspaces/{workspaceId}/targets/{targetId}/suggestionsThe AI-proposed scenarios awaiting review. Each says where it came from: the scope run and requirement it answers, or the discovery that drafted it.
POST /api/workspaces/{workspaceId}/targets/{targetId}/discoveryStart a discovery crawl — quoted by its chosen ceiling before it starts.
POST /api/workspaces/{workspaceId}/targets/discovery/{discoveryId}/proposePropose scenarios from a completed discovery's report.
GET /api/workspaces/{workspaceId}/targets/discoveryList discovery runs, newest first.
GET /api/workspaces/{workspaceId}/targets/discovery/{discoveryId}One discovery's status, without the report.
POST /api/workspaces/{workspaceId}/targets/discovery/{discoveryId}/cancelStop a queued/running discovery — and stop it spending.
GET /api/workspaces/{workspaceId}/targets/discovery/{discoveryId}/detailThe discovery with its screen-map report.
GET /api/workspaces/{workspaceId}/targets/{targetId}/groundingWhich discovery map a compile of this app will be grounded in.
GET /api/workspaces/{workspaceId}/targets/discovery/{discoveryId}/logsThe discovery's engine trace.
GET /api/workspaces/{workspaceId}/targets/discovery/{discoveryId}/pagesThe page sources a phone discovery kept, with typed values taken out: each page's facts.No screen in the app — A kept page is raw material for fixing the engine and building test fixtures, read from the CLI's discovery pages or the MCP server's get_discovery_pages; no screen in the app shows one.
GET /api/workspaces/{workspaceId}/targets/discovery/{discoveryId}/pages/{seq}One kept page: its facts and its XML.No screen in the app — A kept page is raw material for fixing the engine and building test fixtures, read from the CLI's discovery pages or the MCP server's get_discovery_pages; no screen in the app shows one.

Devices

EndpointDoes
GET /api/workspaces/{workspaceId}/devicesThe real devices the farm can lease right now (filter by platform).
GET /api/workspaces/{workspaceId}/nodesThis workspace's own connected machines and their health — per-platform probe outcomes with a reason and a remedy, and every attached-but-unusable device with what to do about it. Not a device picker.
POST /api/workspaces/{workspaceId}/provisionAsk the farm to build an emulator/simulator matching a catalog spec.
GET /api/workspaces/{workspaceId}/provision/{provisioningId}Poll a provision.No screen in the app — polled by the CLI's devices provision-status and the MCP tool get_provision_status after a provision is started; the app has no provisioning flow — its Devices page shows what the farm has, not what it is building.
GET /api/workspaces/{workspaceId}/catalogThe provisionable capability catalog per platform.

Workspace

EndpointDoes
GET /api/workspaces/currentThe key's own workspace {id, name, webAppUrl}; verifies key + config. webAppUrl is the web app this API links to, null when none is set.No screen in the app — what the CLI's whoami calls to check a key and its config; the app knows its workspace from the session and never asks.