Super Farm
On this page

API reference

For developers who call the Super Farm HTTP API from scripts or other programs. The sf command and the web panel are both clients of this API.

The conventions below are written by hand. The route reference after them is generated from the application's OpenAPI schema by scripts/gen_api_docs.py, so it lists every route the daemon serves.

Conventions#

Base URL and format#

The daemon listens on http://127.0.0.1:7700 by default (server.host, server.port). Everything under /api takes and returns JSON, except uploads, downloads and streams. A request body that names a field the route does not know is refused with 422, so a typo never passes silently. Every /api response carries Cache-Control: no-store.

JSON request bodies are limited to 4 MiB (413 too_large above that). Uploads are limited by server.max_upload_mb instead and are checked on the bytes that arrive, not on the length the client announces.

The OpenAPI schema is served at GET /api/openapi.json (authentication required). The interactive documentation pages are off, because they load scripts from a CDN that the panel's content policy forbids. GET /api/config/schema returns the JSON schema of the settings.

Authentication#

Every route needs authentication except GET /api/health, POST /api/auth/login and POST /api/auth/logout. There are two ways in. Both prove knowledge of the same secret, the API token:

  • Scripts send Authorization: Bearer <token>.
  • Browsers log in once with POST /api/auth/login and a body of {"token": "<token>"}, and then carry a session cookie.

Print the token with sf token. It lives in <home>/secrets/api-token with mode 0600. sf token --rotate replaces it, and every browser session and open WebSocket opened with the old one is ended.

The token is never accepted in a URL or query string, and never written to a log. Comparison is constant-time. Wrong tokens are counted per client address: after server.login_attempts_per_minute (10) wrong ones in a minute, that address gets 429 rate_limited with a Retry-After header for the rest of the minute. The right token is always accepted, so another process on the same address cannot lock its holder out.

Whether a caller must authenticate at all is decided from the address the daemon is bound to. With server.auth_required off and a loopback address, requests need no credentials. A daemon bound to any other address always asks for the token.

The token is equivalent to root on the host. See Security model.

Sessions and CSRF#

POST /api/auth/login sets two cookies, both SameSite=Strict:

CookieReadable by the pagePurpose
sf_sessionno (HttpOnly)the session
sf_csrfyesthe CSRF value

The response also returns the CSRF value as csrf. Every request that changes something (any method other than GET, HEAD and OPTIONS) and is authenticated by cookie must send that value in the X-SF-CSRF header, or it is refused with 403 csrf. Requests that use a bearer token do not need it. Sessions last server.session_hours (12) and are kept in memory, so a daemon restart signs every browser out.

Host and Origin checks#

The daemon refuses a request whose Host header is not one it was configured to answer to (403 forbidden). That closes DNS rebinding. Loopback names are always accepted, as are the address in server.host and the hosts of server.allowed_origins. When the daemon listens on all interfaces, any literal IP address is accepted.

A browser request that carries an Origin header must name this server or an entry of server.allowed_origins, or it is refused. Clients that are not browsers send no Origin and are unaffected. CORS headers are added only for the origins listed in server.allowed_origins. Behind a reverse proxy, list the proxy in server.trusted_proxies so the client address in the audit log is the real one.

Errors#

Every error has one shape:

{"error": {"code": "not_found", "message": "there is no Android 7", "detail": null}}

detail is present only when there is more to say. The message is written for the operator. Validation errors list field and message for each problem and never echo the rejected value, because a rejected body can hold a password.

StatuscodeMeaning
400bad_requestA value is wrong.
401unauthorizedNo credentials, or a wrong token.
403forbidden, csrf, immutable_settingUnknown Host, cross-origin request, missing CSRF header, or a settings change the API may not make.
404not_foundNo such Android, app, profile, task or file. An unknown /api path is also 404.
405method_not_allowedThe route exists but not for this method; the Allow header lists the right ones.
409conflict and othersThe farm's current state forbids it. The code says which: busy, over_capacity, needs_force, refused, device_unavailable, screen_offline, screen_booting, h264_unavailable.
413too_largeBody or upload over the limit.
422validation_error, idempotency_key_reusedThe body does not match the route's schema, or an idempotency key was reused with a different request.
429rate_limitedToo many wrong tokens.
500internalA bug. The daemon log has the traceback; the response says nothing about it.
502command_failed, screen_errorDocker, ADB or a device did not do what was asked.
503unavailable, viewer_limitA component cannot run on this machine (the message says why), or stream.max_viewers is reached.
504timeoutA command ran out of time.

Tasks#

Work that touches more than one Android, or takes more than a moment, runs as a background task. The route answers 202 Accepted at once with the task:

{"task": {"id": "t12", "kind": "...", "title": "...", "targets": [1, 2], "state": "queued",
          "current": 0, "total": 2, "message": "", "error": "", "results": {}, "failed": 0,
          "output": null, "created": 0, "started": 0, "finished": 0}}

state is one of queued, running, done, failed and cancelled. results maps each Android id to {"ok": true|false, "detail": "..."}, so one failing Android never hides the others' outcomes. Follow a task with GET /api/tasks/{id} until state is no longer queued or running, list tasks with GET /api/tasks (?active=true, ?limit=), and cancel with POST /api/tasks/{id}/cancel. Tasks live in memory: the daemon keeps the most recent 200 and forgets the rest, and a restart forgets all of them. A task that reports done has finished; look at failed and results for the per-Android outcome.

The sf command does this for you; see --wait in the command reference.

The busy rule#

One operator action per Android at a time. Start, stop, restart, recreate, delete, a manual backup and a restore over an Android hold that Android until their task has finished with it. While it is held, another such request that names it is refused as a whole with 409 busy:

{"error": {"code": "busy", "message": "Android 3 is busy: task t12 (...) is still working on it. ...",
           "detail": {"task": "t12", "ids": [3]}}}

The request is not queued and not merged with the first. A double click therefore gives one task and one refusal that points at it. An Android a task has already finished with is free again while the task goes on with the others. The same rule covers DELETE /api/instances/{id}.

Idempotency#

POST /api/instances accepts an Idempotency-Key header: 1 to 128 printable ASCII characters, no spaces, for example a UUID made when a form opened. Repeating the same request with the same key within ten minutes returns the first answer with the header Idempotency-Replayed: true and creates nothing. Sent at the same moment, the second request waits for the first. The same key with a different body gives 422 idempotency_key_reused. If the first request failed, nothing is remembered. Keys are held in memory, at most 256 of them. Other routes ignore the header.

Capacity and force#

The capacity estimate (GET /api/system/capacity) is advice. With performance.enforce_capacity off (the default) a start over the estimate goes ahead. With it on, start, restart and recreate are refused with 409 over_capacity, which carries the numbers under error.capacity. Repeat the request with "force": true to run anyway. Routes that can lose data (changing an Android's version over existing data, deleting when the safety backup fails) take force the same way and answer 409 needs_force without it. See Scaling.

Settings changes#

PATCH /api/config takes {"patch": {...}}, a JSON merge patch (RFC 7396): objects merge key by key, other values replace, and null removes a key, which puts a setting back to its default. The response returns the new settings plus changed, restart_android and restart_daemon lists. docker.binary, gpu.nvidia.relay_dir and gpu.nvidia.icd decide which program the daemon runs, so a patch that changes them gets 403 immutable_setting. A patch that repeats the current value passes.

Uploads#

Routes that take a file (/api/apps/library, /api/files/push, /api/files/gallery, /api/net/profiles, /api/media) accept either a multipart/form-data body with one file part plus small text fields, or a raw body whose file name comes from ?filename=. The file streams to disk and is cut off when it passes server.max_upload_mb. The client's file name is reduced to a plain safe name.

Screens#

GET /api/instances/{id}/frame.jpg returns one JPEG, GET /api/instances/{id}/stream.mjpg a multipart stream of them. A stream counts as a viewer until the connection ends; stream.max_viewers limits how many run at once. An Android that is not running answers 409 screen_offline, and one still booting 409 screen_booting.

WebSockets#

Two WebSocket routes exist. A socket is accepted when the handshake carries the session cookie or a bearer header. Otherwise the first message must be {"auth": "<token>"} within 10 seconds. A token in the URL closes the socket with code 1008. A socket asks every 5 seconds whether its credential still stands and closes with 1008 when it does not. A browser handshake must pass the same Origin check as a request.

/api/events sends JSON messages, at most a few per second:

MessageWhen
{"type": "hello"}Once, after authentication.
{"type": "update", "tasks": [...], "instances": [...], "removed": [ids]}When tasks or Android states changed. It also carries approvals when an AI approval request was created or decided.
{"type": "resync"}This client fell behind: fetch the state again over HTTP.

Events are a convenience. Everything they report can also be read with GET /api/tasks and GET /api/instances, and a client that only polls works just as well. Fetch the state after hello, then apply updates.

/api/instances/{id}/h264 relays an Android's own H.264 video. The server sends one JSON info message and then binary frames; the client sends JSON control messages (touch, tap, key, text, scroll, back, reset). It needs scrcpy, and the first message tells you when it is off (h264_unavailable). GET /api/stream/capabilities says whether it is offered.

Audit log#

Every request that changes something, or tries to, is written as one JSON line to <home>/logs/audit.log: time, client address, how it authenticated, method, path, status, outcome, duration, and the Androids or task it touched. Request bodies and query strings are never written. Requests nobody authenticated go to audit-anonymous.log, so they cannot push the real history out. An AI bridge can label its calls with an X-SF-Caller header, which appears on the audit line. See server.audit_log, logging.max_mb and logging.keep.

Route reference#

Generated from the application's OpenAPI schema. Every route needs authentication except the ones marked public. Body lists the JSON fields of the request; a field marked required must be present, and fields the route does not list are refused with 422. Routes that start work return 202 with a task (see Tasks).

The API has 197 routes under 24 prefixes and 2 WebSocket routes.

api/health#

GET /api/health#

Health Public: no authentication.

api/auth#

POST /api/auth/login#

Login Public: no authentication.

Body:

FieldTypeNotes
tokentextrequired; length at least 1, length at most 512

POST /api/auth/logout#

Logout Public: no authentication.

GET /api/auth/me#

Me

api/system#

GET /api/system#

System

GET /api/system/capacity#

Capacity

GET /api/system/doctor#

Doctor

ParameterInTypeNotes
freshquerytrue / falseoptional; default False

GET /api/system/metrics#

Metrics

GET /api/system/gpus#

The cards, every rendering choice selectable on this machine (for the per-Android and bulk pickers), and the state of the NVIDIA runtime and relay.

api/device-profiles#

GET /api/device-profiles#

Device Profiles

api/config#

GET /api/config#

Config Get

PATCH /api/config#

Config Patch

Body:

FieldTypeNotes
patchobjectrequired; Nested settings to change, e.g. {"android": {"fps": 25}}.

GET /api/config/schema#

Config Schema

api/images#

GET /api/images#

Images

POST /api/images/{version}/ensure#

Image Ensure

ParameterInTypeNotes
versionpathtextrequired

POST /api/images/{version}/build#

Image Build

ParameterInTypeNotes
versionpathtextrequired

api/tasks#

GET /api/tasks#

Tasks

ParameterInTypeNotes
activequerytrue / falseoptional; default False
limitquerywhole numberoptional; default 50

GET /api/tasks/{task_id}#

Task

ParameterInTypeNotes
task_idpathtextrequired

POST /api/tasks/{task_id}/cancel#

Task Cancel

ParameterInTypeNotes
task_idpathtextrequired

api/net#

GET /api/net/profiles#

Profiles

POST /api/net/profiles#

Profile Upload

DELETE /api/net/profiles/{name}#

Profile Delete

ParameterInTypeNotes
namepathtextrequired

GET /api/net/credentials#

Names only. There is no way to read a stored username or password back.

PUT /api/net/credentials/{name}#

Credentials Set

ParameterInTypeNotes
namepathtextrequired

Body:

FieldTypeNotes
usernametextrequired; length at least 1, length at most 256
passwordtextrequired; length at least 1, length at most 1024

DELETE /api/net/credentials/{name}#

Credentials Delete

ParameterInTypeNotes
namepathtextrequired

GET /api/net/gateways#

Gateways

PUT /api/net/gateways/{name}#

Gateway Put

ParameterInTypeNotes
namepathtextrequired

Body:

FieldTypeNotes
provideropenvpn or wireguardoptional; default "openvpn"
profiletextrequired; length at least 1, length at most 200
credentialstextoptional; default "default"; length at most 64
subnettextoptional; default ""; length at most 43

DELETE /api/net/gateways/{name}#

Gateway Delete

ParameterInTypeNotes
namepathtextrequired
forcequerytrue / falseoptional; default False

POST /api/net/gateways/{name}/{action}#

Gateway Action

ParameterInTypeNotes
namepathtextrequired
actionpathup or downrequired
forcequerytrue / falseoptional; default False

api/instances#

GET /api/instances/{id}/net#

Mode, tunnel state, kill switch and the public address. refresh asks the address again from inside the Android's network (a few seconds) instead of showing the last known one.

ParameterInTypeNotes
idpathtextrequired
refreshquerytrue / falseoptional; default False

GET /api/instances/{id}/apps#

Instance Apps

ParameterInTypeNotes
idpathtextrequired
third_partyquerytrue / falseoptional; default True

GET /api/instances/{id}/apps/{package}/permissions#

Instance App Permissions

ParameterInTypeNotes
idpathtextrequired
packagepathtextrequired

GET /api/instances/{id}/files#

Files List

ParameterInTypeNotes
idpathtextrequired
pathquerytextoptional; default /sdcard

GET /api/instances/{id}/files/pull#

Files Pull

ParameterInTypeNotes
idpathtextrequired
pathquerytextrequired

GET /api/instances/{id}/info#

Device Info

ParameterInTypeNotes
idpathtextrequired

GET /api/instances/{id}/screenshot#

Device Screenshot

ParameterInTypeNotes
idpathtextrequired

GET /api/instances/{id}/logcat#

filter keeps the lines containing that text; priority is the minimum level (V D I W E F).

ParameterInTypeNotes
idpathtextrequired
linesquerywhole numberoptional; default 200
filterquerytext (may be null)optional
priorityquerytext (may be null)optional
packagequerytext (may be null)optional

GET /api/instances/{id}/frame.jpg#

Frame

ParameterInTypeNotes
idpathtextrequired
widthquerywhole number (may be null)optional
max_agequerynumberoptional; default 1.0

GET /api/instances/{id}/screen#

Screen Status

ParameterInTypeNotes
idpathtextrequired

GET /api/instances/{id}/stream.mjpg#

Mjpeg

ParameterInTypeNotes
idpathtextrequired
fpsquerynumber (may be null)optional
widthquerywhole number (may be null)optional

GET /api/instances#

List Instances

POST /api/instances#

Add Androids. With an Idempotency-Key header (any 1-128 printable characters, e.g. a UUID made when the form was opened) a repeat of the same request within ten minutes returns the first answer, marked Idempotency-Replayed: true, and creates nothing.

Body:

FieldTypeNotes
countwhole numberoptional; default 1; at least 1, at most 500
nametextoptional; default ""; length at most 60
templateobjectoptional; InstanceSpec fields shared by the new Androids (android_version, gpu_mode, network, tags, ...).

POST /api/instances/bulk#

Bulk

Body:

FieldTypeNotes
actionstart or stop or restart or recreate or deleterequired
selectortextrequired; length at least 1, length at most 500
forcetrue / falseoptional; default False

GET /api/instances/{id}#

Status

ParameterInTypeNotes
idpathtextrequired

PATCH /api/instances/{id}#

Update

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
patchobjectrequired; InstanceSpec fields to change; applied at the next (re)start.
forcetrue / falseoptional; default False; Allow a change that can lose data, such as another Android version on an existing /data.

DELETE /api/instances/{id}#

Delete

ParameterInTypeNotes
idpathtextrequired
keep_dataquerytrue / falseoptional; default False
forcequerytrue / falseoptional; default False

POST /api/instances/{id}/clone#

Clone

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
with_datatrue / falseoptional; default False

GET /api/instances/{id}/logs#

Logs

ParameterInTypeNotes
idpathtextrequired
tailquerywhole numberoptional; default 200

POST /api/instances/{id}/{action}#

Lifecycle

ParameterInTypeNotes
idpathtextrequired
actionpathstart or stop or restart or recreaterequired

Body: see the route's description.

api/apps#

GET /api/apps/library#

Library

POST /api/apps/library#

Library Upload

DELETE /api/apps/library/{app_id}#

Library Delete

ParameterInTypeNotes
app_idpathtextrequired

POST /api/apps/install#

App Install

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
app_idtextrequired; length at least 1, length at most 200
granttrue / falseoptional; default True
replacetrue / falseoptional; default True
downgradetrue / falseoptional; default False
all_localestrue / falseoptional; default False

POST /api/apps/ensure#

App Ensure

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
app_idtextrequired; length at least 1, length at most 200
granttrue / falseoptional; default True

POST /api/apps/uninstall#

App Uninstall

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
packagetextrequired; length at least 1, length at most 255
keep_datatrue / falseoptional; default False

POST /api/apps/launch#

App Launch

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
packagetextrequired; length at least 1, length at most 255
activitytext (may be null)optional

POST /api/apps/stop#

App Stop

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
packagetextrequired; length at least 1, length at most 255

POST /api/apps/clear#

App Clear

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
packagetextrequired; length at least 1, length at most 255

POST /api/apps/enable#

App Enable

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
packagetextrequired; length at least 1, length at most 255

POST /api/apps/disable#

App Disable

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
packagetextrequired; length at least 1, length at most 255

POST /api/apps/grant#

App Grant

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
packagetextrequired; length at least 1, length at most 255
permissiontextoptional; default "all"; length at most 200; One permission name, or all for every runtime permission the app requests.

POST /api/apps/revoke#

App Revoke

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
packagetextrequired; length at least 1, length at most 255
permissiontextoptional; default "all"; length at most 200; One permission name, or all for every runtime permission the app requests.

GET /api/apps/matrix#

package -> {instance id: version}. With detail, also version codes, enabled flags and the Androids that could not be read.

ParameterInTypeNotes
selectorquerytextoptional; default all
detailquerytrue / falseoptional; default False

api/files#

POST /api/files/push#

Upload one file and push it to every selected Android. Fields: selector, and remote (a folder ending in /, or a full path; default /sdcard/Download/).

POST /api/files/gallery#

Upload one picture or video and add it to the gallery of every selected Android.

api/device#

POST /api/device/locale#

Device Locale

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
localetextrequired; length at least 2, length at most 35
restarttrue / falseoptional; default True; Restart the Android framework so every app sees the new language.

POST /api/device/timezone#

Device Timezone

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
timezonetextrequired; length at least 1, length at most 100

POST /api/device/geo#

Device Geo

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
latnumber (may be null)optional; Latitude. Leave out (with lon) to remove the mock location.
lonnumber (may be null)optional
accuracynumberoptional; default 10.0; more than 0, at most 10000

POST /api/device/proxy#

Device Proxy

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
proxytextoptional; default ""; length at most 300; host:port, or empty to switch the proxy off.

POST /api/device/clipboard#

Device Clipboard

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
texttextrequired; length at least 1

POST /api/device/animations#

Device Animations

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
enabledtrue / falserequired

POST /api/device/dark-mode#

Device Dark Mode

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
enabledtrue / falserequired

POST /api/device/font-scale#

Device Font Scale

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
scalenumberrequired; at least 0.5, at most 2

POST /api/device/open-url#

Device Open Url

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
urltextrequired; length at least 1, length at most 2048
packagetext (may be null)optional

POST /api/device/wake#

Device Wake

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated

POST /api/device/reboot#

Device Reboot

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
modetextoptional; default "soft"; soft: restart the Android framework. container: restart the containers.
forcetrue / falseoptional; default False

api/shell#

POST /api/shell#

Shell Run

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated
argvlist of textrequired; at most items 256; The command as separate arguments; nothing is passed through a shell on the host.
timeoutnumber (may be null)optional
roottrue / falseoptional; default False
forcetrue / falseoptional; default False; Run a command the denylist would refuse.

GET /api/shell/{task_id}/output#

Shell Output

ParameterInTypeNotes
task_idpathtextrequired

api/stream#

GET /api/stream/capabilities#

Which ways of watching a device this host offers, so the panel can pick one.

api/input#

POST /api/input/tap#

Input Tap

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional
xnumberrequired
ynumberrequired
normalizedtrue / falseoptional; default True; Coordinates are 0..1 of the screen, so one gesture fits devices of any resolution.

POST /api/input/swipe#

Input Swipe

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional
x1numberrequired
y1numberrequired
x2numberrequired
y2numberrequired
mswhole numberoptional; default 300; at least 1, at most 30000
normalizedtrue / falseoptional; default True

POST /api/input/long-press#

Input Long Press

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional
xnumberrequired
ynumberrequired
normalizedtrue / falseoptional; default True; Coordinates are 0..1 of the screen, so one gesture fits devices of any resolution.
mswhole numberoptional; default 700; at least 1, at most 30000

POST /api/input/text#

Input Text

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional
valuetextrequired; length at least 1, length at most 10000

POST /api/input/key#

Input Key

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional
nametext or whole numberrequired; A key name (HOME, BACK, ENTER, ...) or an Android key code.

api/windows#

GET /api/windows#

Windows

POST /api/windows/close-all#

Windows Close All

POST /api/windows/{action}#

Windows Action

ParameterInTypeNotes
actionpathopen or close or gridrequired

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional

api/camera#

GET /api/camera#

Camera

ParameterInTypeNotes
androidquerytrue / falseoptional; default False

GET /api/camera/sources#

Camera Sources

POST /api/camera/play#

Camera Play

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional
sourcetextrequired; length at least 1, length at most 200; A file name from the media library, or rtmp for the live ingest.
soundtextoptional; default "source"; length at most 200; source | none | a library file whose sound plays next to the picture.

POST /api/camera/stop#

Camera Stop

Body: see the route's description.

POST /api/camera/mic#

Camera Mic

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional
modemute or unmute or solorequired

POST /api/camera/attach#

Camera Attach

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional

POST /api/camera/install-hal#

Camera Install Hal

Body:

FieldTypeNotes
idslist of whole number (may be null)optional
selectortext (may be null)optional

api/media#

POST /api/media#

Add a video or sound file to the media library (the camera's sources).

DELETE /api/media/{name}#

Media Delete

ParameterInTypeNotes
namepathtextrequired

api/listen#

GET /api/listen#

Listen Status

DELETE /api/listen#

Listen Stop

POST /api/listen/{id}#

Listen Start

ParameterInTypeNotes
idpathtextrequired

api/automation#

GET /api/automation/datasets#

Datasets

POST /api/automation/datasets#

Dataset Create

Body:

FieldTypeNotes
nametextrequired; length at least 1, length at most 120
descriptiontextoptional; default ""; length at most 4000
packagetextoptional; default ""; length at most 200

GET /api/automation/datasets/{slug}#

Dataset

ParameterInTypeNotes
slugpathtextrequired

PATCH /api/automation/datasets/{slug}#

Dataset Update

ParameterInTypeNotes
slugpathtextrequired

Body:

FieldTypeNotes
nametext (may be null)optional
descriptiontext (may be null)optional
packagetext (may be null)optional

DELETE /api/automation/datasets/{slug}#

Dataset Delete

ParameterInTypeNotes
slugpathtextrequired

POST /api/automation/datasets/{slug}/train#

Dataset Train

ParameterInTypeNotes
slugpathtextrequired

GET /api/automation/datasets/{slug}/recordings/{rid}#

Recording

ParameterInTypeNotes
slugpathtextrequired
ridpathtextrequired

DELETE /api/automation/datasets/{slug}/recordings/{rid}#

Recording Delete

ParameterInTypeNotes
slugpathtextrequired
ridpathtextrequired

GET /api/automation/datasets/{slug}/recordings/{rid}/shots/{relative}#

Recording Shot

ParameterInTypeNotes
slugpathtextrequired
ridpathtextrequired
relativepathtextrequired

GET /api/automation/record#

Record Active

POST /api/automation/record/start#

Record Start

Body:

FieldTypeNotes
datasettextrequired; length at least 1, length at most 64
idwhole numberrequired

GET /api/automation/record/{id}#

Record Status

ParameterInTypeNotes
idpathtextrequired

POST /api/automation/record/{id}/tap#

Record Tap

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
xnumberrequired
ynumberrequired
normalizedtrue / falseoptional; default True

POST /api/automation/record/{id}/long-press#

Record Long Press

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
xnumberrequired
ynumberrequired
normalizedtrue / falseoptional; default True
mswhole numberoptional; default 700; at least 1, at most 30000

POST /api/automation/record/{id}/swipe#

Record Swipe

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
x1numberrequired
y1numberrequired
x2numberrequired
y2numberrequired
mswhole numberoptional; default 300; at least 1, at most 30000
normalizedtrue / falseoptional; default True

POST /api/automation/record/{id}/text#

Record Text

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
valuetext (may be null)optional; Text typed now and stored as written.
nametext (may be null)optional; Record a placeholder {name} instead; the value comes from the run's values.

POST /api/automation/record/{id}/key#

Record Key

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
nametextrequired; length at least 1, length at most 40

POST /api/automation/record/{id}/launch#

Record Launch

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
packagetextrequired; length at least 1, length at most 200

POST /api/automation/record/{id}/wait#

Record Wait

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
secondsnumberrequired; more than 0, at most 600

POST /api/automation/record/{id}/assert-visible#

Record Assert Visible

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
xnumberrequired
ynumberrequired
normalizedtrue / falseoptional; default True

POST /api/automation/record/{id}/undo#

Record Undo

ParameterInTypeNotes
idpathtextrequired

POST /api/automation/record/{id}/stop#

Record Stop

ParameterInTypeNotes
idpathtextrequired

POST /api/automation/record/{id}/cancel#

Record Cancel

ParameterInTypeNotes
idpathtextrequired

GET /api/automation/programs#

Programs

GET /api/automation/programs/{slug}#

Program

ParameterInTypeNotes
slugpathtextrequired

DELETE /api/automation/programs/{slug}#

Program Delete

ParameterInTypeNotes
slugpathtextrequired

GET /api/automation/programs/{slug}/shots/{relative}#

Program Shot

ParameterInTypeNotes
slugpathtextrequired
relativepathtextrequired

GET /api/automation/flows#

Flows

POST /api/automation/flows/validate#

400 with the reason when the flow is not valid; the summary when it is.

Body:

FieldTypeNotes
texttext (may be null)optional; YAML to check without saving it.
nametext (may be null)optional; A saved flow to check in full: flows it calls, and roles it uses.

GET /api/automation/flows/{name}#

Flow Get

ParameterInTypeNotes
namepathtextrequired

PUT /api/automation/flows/{name}#

Flow Put

ParameterInTypeNotes
namepathtextrequired

Body:

FieldTypeNotes
texttextrequired; length at least 1, length at most 524288

DELETE /api/automation/flows/{name}#

Flow Delete

ParameterInTypeNotes
namepathtextrequired

GET /api/automation/profiles#

Profiles

GET /api/automation/profiles/{package}#

Profile Get

ParameterInTypeNotes
packagepathtextrequired

PUT /api/automation/profiles/{package}#

Profile Put

ParameterInTypeNotes
packagepathtextrequired

Body:

FieldTypeNotes
rolesobjectoptional
overlayslist of objectoptional
never_taplist of anyoptional
notestextoptional; default ""; length at most 4000

DELETE /api/automation/profiles/{package}#

Profile Delete

ParameterInTypeNotes
packagepathtextrequired

PUT /api/automation/profiles/{package}/roles/{role}#

Profile Role Put

ParameterInTypeNotes
packagepathtextrequired
rolepathtextrequired

Body:

FieldTypeNotes
selectorobject or textrequired

DELETE /api/automation/profiles/{package}/roles/{role}#

Profile Role Delete

ParameterInTypeNotes
packagepathtextrequired
rolepathtextrequired

POST /api/automation/profiles/{package}/overlays#

Profile Overlay Add

ParameterInTypeNotes
packagepathtextrequired

Body:

FieldTypeNotes
ruleobjectrequired

DELETE /api/automation/profiles/{package}/overlays/{name}#

Profile Overlay Delete

ParameterInTypeNotes
packagepathtextrequired
namepathtextrequired

POST /api/automation/profiles/{package}/never-tap#

Profile Never Tap Add

ParameterInTypeNotes
packagepathtextrequired

Body:

FieldTypeNotes
selectorobject or textrequired

DELETE /api/automation/profiles/{package}/never-tap/{index}#

Profile Never Tap Delete

ParameterInTypeNotes
packagepathtextrequired
indexpathwhole numberrequired

POST /api/automation/profiles/{package}/teach#

Profile Teach

ParameterInTypeNotes
packagepathtextrequired

Body:

FieldTypeNotes
idwhole numberrequired
roletextrequired; length at least 1, length at most 60
xwhole numberrequired
ywhole numberrequired
with_texttrue / falseoptional; default True

GET /api/automation/values#

Values

PUT /api/automation/values/shared/{name}#

Value Shared Put

ParameterInTypeNotes
namepathtextrequired

Body:

FieldTypeNotes
valuetextrequired; length at most 100000

DELETE /api/automation/values/shared/{name}#

Value Shared Delete

ParameterInTypeNotes
namepathtextrequired

GET /api/automation/values/instances/{id}#

Values Instance

ParameterInTypeNotes
idpathtextrequired

PUT /api/automation/values/instances/{id}/{name}#

Value Instance Put

ParameterInTypeNotes
idpathtextrequired
namepathtextrequired

Body:

FieldTypeNotes
valuetextrequired; length at most 100000

DELETE /api/automation/values/instances/{id}/{name}#

Value Instance Delete

ParameterInTypeNotes
idpathtextrequired
namepathtextrequired

GET /api/automation/values/pools#

Pools

GET /api/automation/values/pools/{name}#

Pool

ParameterInTypeNotes
namepathtextrequired
limitquerywhole numberoptional; default 50

POST /api/automation/values/pools/{name}#

Pool Add

ParameterInTypeNotes
namepathtextrequired

Body:

FieldTypeNotes
lineslist of textrequired; at most items 20000

DELETE /api/automation/values/pools/{name}#

Pool Delete

ParameterInTypeNotes
namepathtextrequired

GET /api/automation/runs#

Runs

ParameterInTypeNotes
activequerytrue / falseoptional; default False

POST /api/automation/runs#

Run Start

Body:

FieldTypeNotes
kindprogram or flowrequired
nametextrequired; length at least 1, length at most 80
selectortextrequired; length at least 1, length at most 500
variablesmap of text to textoptional; Values for this run only; kept in memory, never written to disk.
dry_runtrue / falseoptional; default False

GET /api/automation/runs/{task_id}#

Run Status

ParameterInTypeNotes
task_idpathtextrequired

POST /api/automation/runs/{task_id}/cancel#

Run Cancel

ParameterInTypeNotes
task_idpathtextrequired

Body: see the route's description.

POST /api/automation/runs/{task_id}/resolve#

Run Resolve

ParameterInTypeNotes
task_idpathtextrequired

Body:

FieldTypeNotes
idwhole numberrequired
actionresume or skip or abortoptional; default "resume"

GET /api/automation/schedules#

Schedules

POST /api/automation/schedules#

Schedule Create

Body:

FieldTypeNotes
nametextrequired; length at least 1, length at most 80
kindprogram or flowrequired
targettextrequired; length at least 1, length at most 80
selectortextrequired; length at least 1, length at most 500
everywhole numberoptional; default 0; at least 0; Seconds between runs. Use this or cron.
crontextoptional; default ""; length at most 120; minute hour day-of-month month day-of-week.
variablesmap of text to textoptional
dry_runtrue / falseoptional; default False
enabledtrue / falseoptional; default True

PATCH /api/automation/schedules/{schedule_id}#

Schedule Update

ParameterInTypeNotes
schedule_idpathtextrequired

Body:

FieldTypeNotes
nametext (may be null)optional
kindprogram or flow (may be null)optional
targettext (may be null)optional
selectortext (may be null)optional
everywhole number (may be null)optional
crontext (may be null)optional
variablesmap of text to text (may be null)optional
dry_runtrue / false (may be null)optional
enabledtrue / false (may be null)optional

DELETE /api/automation/schedules/{schedule_id}#

Schedule Delete

ParameterInTypeNotes
schedule_idpathtextrequired

GET /api/automation/screen/{id}#

Screen

ParameterInTypeNotes
idpathtextrequired

GET /api/automation/screen/{id}/element#

Screen Element

ParameterInTypeNotes
idpathtextrequired
xquerywhole numberrequired
yquerywhole numberrequired

GET /api/automation/llm#

Llm

api/backups#

GET /api/backups#

Backups

POST /api/backups#

Backup Create

Body:

FieldTypeNotes
selectortextrequired; length at least 1, length at most 500; Which Androids: all | running | stopped | tag:x | 5 | 1-10 | comma-separated

DELETE /api/backups#

The name comes in the body or as ?archive= (not every client sends a body with DELETE).

ParameterInTypeNotes
archivequerytext (may be null)optional

Body: see the route's description.

POST /api/backups/farm#

Backup Farm

Body: see the route's description.

POST /api/backups/verify#

Backup Verify

Body:

FieldTypeNotes
archivetextrequired; length at least 1, length at most 80; A backup name as listed: android-<id>/<time> or farm/<time>.
deeptrue / falseoptional; default False; Also read the data archive end to end (slow).

POST /api/backups/prune#

Backup Prune

POST /api/backups/restore#

Backup Restore

Body:

FieldTypeNotes
archivetextrequired; length at least 1, length at most 80; A backup name as listed: android-<id>/<time> or farm/<time>.
idwhole number (may be null)optional; Restore over this Android (it must be stopped; needs force). Leave out to restore as a new Android.
forcetrue / falseoptional; default False
keep_networktrue / falseoptional; default False; Restoring as new next to the original: keep the copy's network on. Off by default so one identity is never online twice.
trusttrue / falseoptional; default False; Farm backup made by another farm: also take custom images, boot arguments, properties, adopted volumes, state and schedules from it. Off by default because those can start a privileged container of the archive's choosing.
restore_configtrue / falseoptional; default False; Farm backup: also replace this farm's config.yaml with the archived one.
restore_secretstrue / falseoptional; default False; Farm backup: also restore VPN profiles, credentials and tokens from the archive.

GET /api/backups/download#

Backup Download

ParameterInTypeNotes
archivequerytextrequired

api/approvals#

GET /api/approvals#

Approvals

ParameterInTypeNotes
statequerytext (may be null)optional
limitquerywhole numberoptional; default 100

POST /api/approvals#

Called by an AI bridge before every changing or destructive tool call. targets in the answer are the Androids the selector resolved to here; the bridge acts on exactly those.

Body:

FieldTypeNotes
tooltextrequired; length at least 1, length at most 64
argumentsobjectoptional; The tool's arguments, as the assistant sent them.
callertextrequired; length at least 1, length at most 80; Who asks, e.g. mcp:<client name>.
sessiontextrequired; length at least 1, length at most 80; Opaque id of the assistant's session; approve and remember is tied to it.

DELETE /api/approvals/sessions/{session}#

Session End

ParameterInTypeNotes
sessionpathtextrequired

GET /api/approvals/{id}#

Approval

ParameterInTypeNotes
idpathtextrequired

GET /api/approvals/{id}/wait#

Returns as soon as the request is no longer pending, or after timeout seconds with its state as it is. Waiting here holds no worker thread, so many assistants can wait.

ParameterInTypeNotes
idpathtextrequired
timeoutquerynumberoptional; default 20.0

POST /api/approvals/{id}/approve#

Approve

ParameterInTypeNotes
idpathtextrequired

Body: see the route's description.

POST /api/approvals/{id}/deny#

Deny

ParameterInTypeNotes
idpathtextrequired

Body: see the route's description.

POST /api/approvals/{id}/cancel#

Cancel

ParameterInTypeNotes
idpathtextrequired

api/mcp#

GET /api/mcp/info#

What AI assistants may do on this farm, and how to connect one.

api/ai#

GET /api/ai/models#

Model List

POST /api/ai/models/{name}/test#

Model Test

ParameterInTypeNotes
namepathtextrequired

GET /api/ai/conversations#

Conversations

POST /api/ai/conversations#

Conversation Create

Body: see the route's description.

GET /api/ai/conversations/{id}#

Conversation

ParameterInTypeNotes
idpathtextrequired
afterquerywhole numberoptional; default 0

DELETE /api/ai/conversations/{id}#

Conversation Delete

ParameterInTypeNotes
idpathtextrequired

POST /api/ai/conversations/{id}/messages#

Conversation Message

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
texttextrequired; length at least 1, length at most 8000

POST /api/ai/conversations/{id}/cancel#

Conversation Cancel

ParameterInTypeNotes
idpathtextrequired

POST /api/ai/pilot#

Pilot Start

Body:

FieldTypeNotes
goaltextrequired; length at least 3, length at most 600
packagetextrequired; length at least 3, length at most 200
selectortextrequired; length at least 1, length at most 500
modeltext (may be null)optional
successtext or object (may be null)optional; text:..., id:..., desc:..., regex:..., package:..., activity:..., or a selector mapping
max_stepswhole number (may be null)optional
datasettext (may be null)optional
runswhole numberoptional; default 1; at least 1, at most 10
valuesmap of text to text (may be null)optional; Named values the Pilot may type (kept in memory for this session only)
allowed_packageslist of text (may be null)optional

GET /api/ai/pilot/sessions#

Pilot Sessions

ParameterInTypeNotes
activequerytrue / falseoptional; default False

GET /api/ai/pilot/sessions/{id}#

Pilot Session

ParameterInTypeNotes
idpathtextrequired

POST /api/ai/pilot/sessions/{id}/cancel#

Pilot Cancel

ParameterInTypeNotes
idpathtextrequired

Body: see the route's description.

POST /api/ai/pilot/sessions/{id}/devices/{instance}/resolve#

Pilot Resolve

ParameterInTypeNotes
idpathtextrequired
instancepathtextrequired

Body: see the route's description.

POST /api/ai/pilot/sessions/{id}/confirm#

Pilot Confirm

ParameterInTypeNotes
idpathtextrequired

Body:

FieldTypeNotes
instancewhole numberrequired
recordingtext (may be null)optional

POST /api/ai/pilot/{dataset}/train#

Pilot Train

ParameterInTypeNotes
datasetpathtextrequired

GET /api/ai/pilot/{dataset}/summary#

Pilot Summary

ParameterInTypeNotes
datasetpathtextrequired

WebSocket routes#

PathNotes
/api/eventsSee WebSockets.
/api/instances/{id}/h264See WebSockets.

Next#

This page is docs/API.md in the repository.