HTTP API

The same operations as the MCP tools, over plain HTTPS and JSON. The API is in preview, so expect additions.

Base URL
https://api.distilled.cx
Auth
Authorization: Bearer dk_live_a1b2c3d4e5f6g7h8_<secret>

Authentication

Every call needs an API key. A key carries scopes, and each endpoint lists the scope it needs. Check a key with:

curl https://api.distilled.cx/v1/me -H "Authorization: Bearer $DISTILLED_API_KEY"

Scopes

ScopeGrants
phones:readList phones and read their state.
phones:writeAllocate phones onto the rack.
apps:writeInstall an app on a phone.
runs:readList and inspect runs, and read their events.
runs:writeStart and stop runs.
skills:readList registered skills and cohorts.
skills:writeCreate, change, and delete skills and cohorts.

Retries

Send an Idempotency-Key header on calls that change state to make retries safe. Reusing a key with a different body returns idempotency_conflict. On rate_limited, wait for the Retry-After header.

Endpoints

Account

GET/v1/me

Identify the workspace and scopes behind the key. Use it to check a key works.

Scope any · Returns workspace, and the key's id, name, scopes, and expiry

Phones

GET/v1/phones

Every phone in the workspace.

Scope phones:read · Returns phone[]

GET/v1/phones/:id/apps

Apps installed on a phone that a run can open. The catalogue is what you may install; this is what is there now.

Scope phones:read · Returns the phone id, its launchable packages, and when its node last measured them

POST/v1/phones/allocate

Claim phones from the warm pool, up to the workspace quota.

Scope phones:write · Returns phone[], or 409 capacity_unavailable when the pool is empty

FieldTypeDescription
countintegerHow many phones to claim, 1 to 20. Default 1.
devicestringDevice profile, such as pixel8pro.
regionstringWhere the phone should run.

GET/v1/phones/:id

One phone by id.

Scope phones:read · Returns phone

FieldTypeDescription
idrequiredstringPhone id.

POST/v1/phones/:id/release

Hand a phone back to the pool. It leaves the workspace at once and is wiped and re-identified before anyone can claim it again.

Scope phones:write · Returns { id, released: true }

FieldTypeDescription
idrequiredstringPhone id.

Apps

GET/v1/apps/search

Search the app catalogue.

Scope phones:read · Returns app[]

FieldTypeDescription
qrequiredstringSearch text, as a query parameter.

GET/v1/apps/:pkg

One catalogue app by Android package name.

Scope phones:read · Returns app

FieldTypeDescription
pkgrequiredstringAndroid package, such as com.example.app.

POST/v1/apps/install

Install a pinned app on chosen phones. Returns a run you can poll or stream.

Scope apps:write · Returns run

FieldTypeDescription
pkgrequiredstringAndroid package to install.
phonesstring[]Phone ids to install on. Omit to target every phone in the workspace.

Skills

GET/v1/skills

Registered workflows.

Scope skills:read · Returns skill[]

POST/v1/skills

Register a workflow. The block list is validated, with depth and size limits.

Scope skills:write · Returns skill

FieldTypeDescription
namerequiredstringWhat the workflow is called.
apprequiredstringAndroid package the workflow drives.
intentstringOne sentence on what the workflow is for.
workflowrequiredobject[]The blocks to run: step, wait, repeat, or if.
guardrailsobjectTime limit, stop conditions, and phone cap.
riskstringlow, medium, or high.
targetCohortstringCohort the workflow is meant for.

POST/v1/skills/:id/enabled

Enable or disable a registered workflow without deleting it.

Scope skills:write · Returns skill

FieldTypeDescription
idrequiredstringSkill id.
enabledrequiredbooleanWhether runs may use it.

DELETE/v1/skills/:id

Delete a registered workflow.

Scope skills:write · Returns { deleted: true }

FieldTypeDescription
idrequiredstringSkill id.

Cohorts

GET/v1/cohorts

Saved groups used to target runs.

Scope phones:read · Returns cohort[]

POST/v1/cohorts

Save a group of phones by region or device to target later.

Scope phones:write · Returns cohort

FieldTypeDescription
namerequiredstringUnique within the workspace.
filterobjectAny of region (US or EU), model (device profile id), and app (Android package).

DELETE/v1/cohorts/:id

Delete a cohort.

Scope phones:write · Returns { deleted: true }

FieldTypeDescription
idrequiredstringCohort id.

GET/v1/cohorts/:id/phones

The phones a cohort currently matches.

Scope phones:read · Returns phone[]

FieldTypeDescription
idrequiredstringCohort id.

Runs

GET/v1/runs

Recent runs, newest first.

Scope runs:read · Returns run[]

POST/v1/runs

Start a run. It returns immediately; poll the run or stream its events.

Scope runs:write · Returns run

FieldTypeDescription
skillrequiredstring | objectA registered skill id, or a workflow defined inline.
targetstringCohort name. Omit to target every live phone.
phonesstring[]Phone ids, instead of a cohort.

GET/v1/runs/:id

One run, with its per-step state.

Scope runs:read · Returns run

FieldTypeDescription
idrequiredstringRun id.

GET/v1/runs/:id/events

Server-sent events for a run. Resume with Last-Event-ID.

Scope runs:read · Returns text/event-stream of run events

FieldTypeDescription
idrequiredstringRun id.

POST/v1/runs/:id/stop

Stop a run that is still going.

Scope runs:write · Returns run

FieldTypeDescription
idrequiredstringRun id.

Errors

Failures return a JSON body with a stable code.

CodeMeaning
invalid_requestThe body or a parameter failed validation.
unauthorizedThe key is missing, malformed, revoked, or expired.
insufficient_scopeThe key is valid but lacks the scope this call needs.
not_invitedThe account is not a member of an active workspace.
not_foundNo such resource in this workspace.
idempotency_conflictThe same Idempotency-Key was reused with a different body.
capacity_unavailableNo phone was free to claim.
quota_exceededThe workspace is at its phone quota.
rate_limitedToo many calls. Retry after the Retry-After header.
unavailableA dependency is down. Retrying is safe.