Which account this key belongs to, and the ids of the devices it controls. A good first call to check your key works.
curl -s -H "X-API-Key: $ERIUS_KEY" \
$ERIUS_API/whoami{
"tenant": "your-team",
"keyId": "k1",
"devices": ["dev_7f3a"]
}Docs · Device API
Every Erius Phone device is driven through one small JSON API: read the screen, tap, type, swipe, press keys, open apps and install your APK. This page documents the API as it runs today.
Overview
Each device is a real Android 13 phone running in its own isolated container. The API gives your agent the same surface a person has — the touchscreen and the hardware keys — plus a structured read of whatever is on screen. Any language or agent framework that can make an HTTP request can use it.
Requests are JSON (Content-Type: application/json). Responses are JSON, except /screenshot, which returns a PNG, and /snapshot?format=text, which returns plain text.
Every URL that acts on a phone starts with /device/:id. Your device ids come from GET /whoami or GET /devices. During early access, devices are provisioned for you — there's no create-device endpoint.
/snapshot returns the accessibility tree, with [ref=N] on everything you can act on. Pass {"ref": N} to tap it. Refs reset on every snapshot, so act on a fresh one.
Instead of a ref, you can target by label: {"text": "Search"} or {"contentDesc": "Settings"}. The API takes a fresh snapshot, picks the best match, and taps its nearest tappable parent.
For anything the tree can't see (WebViews, Flutter, games), take a /screenshot and tap with {"x", "y"} in screen pixels (720×1280).
Authentication
Every request except GET /healthz needs your API key, sent as X-API-Key: <key> or as Authorization: Bearer <key>. A missing or unknown key gets 401 unauthorized.
Each key belongs to exactly one account. It can see and control only that account's devices: /devices lists only yours, and any device id that isn't yours — whether or not it exists — gets the same 403 forbidden response, so other accounts' devices can't be discovered by guessing ids.
We store only a hash of your key, never the key itself, and keys are never written to logs or returned by the API. An account can hold more than one key, so a key can be rotated without downtime: we issue the new one, you switch over, the old one is revoked.
Getting a key: not self-serve yet.
There's no public signup or dashboard during early access. Keys are issued by us, together with your device and the base URL to call. Request early access and we'll get in touch.
Getting started
The basic agent loop in five calls: find your phone, see the screen, tap something, check what happened.
# the key and base URL you received with early access
export ERIUS_KEY="<your api key>"
export ERIUS_API="<your base url>"
# 1. which devices does this key control?
curl -s -H "X-API-Key: $ERIUS_KEY" $ERIUS_API/whoami
# → {"tenant": "your-team", "keyId": "k1", "devices": ["dev_7f3a"]}
# 2. take a screenshot (PNG, 720×1280)
curl -s -H "X-API-Key: $ERIUS_KEY" -o step_01.png $ERIUS_API/device/dev_7f3a/screenshot
# 3. tap a coordinate you picked from the screenshot
curl -s -H "X-API-Key: $ERIUS_KEY" -H 'Content-Type: application/json' \
-d '{"x": 360, "y": 640}' $ERIUS_API/device/dev_7f3a/tap
# → {"ok": true}
# 4. or read the screen as a tree and tap by label instead
curl -s -H "X-API-Key: $ERIUS_KEY" "$ERIUS_API/device/dev_7f3a/snapshot?format=text"
curl -s -H "X-API-Key: $ERIUS_KEY" -H 'Content-Type: application/json' \
-d '{"text": "Search"}' $ERIUS_API/device/dev_7f3a/tap
# 5. screenshot again to see the result
curl -s -H "X-API-Key: $ERIUS_KEY" -o step_02.png $ERIUS_API/device/dev_7f3a/screenshot
Device id dev_7f3a and tenant name are examples; use the ones returned by /whoami.
Errors
HTTP/1.1 403 Forbidden
{
"ok": false,
"error": "forbidden",
"message": "device not found or not accessible with this key"
}
| Status | Codes you'll see | Meaning |
|---|---|---|
400 | bad_request, bad_json, unsupported_text, not_an_apk | Bad input — the message says which field. |
401 | unauthorized | Missing or wrong API key. |
403 | forbidden | Device isn't yours, or doesn't exist (deliberately the same). |
404 | SelectorNotFound, ElementNotFound, not_installed, not_found | Nothing on screen matched, unknown ref, package not installed, unknown route. |
408 | WaitTimeout | /wait didn't see the text in time. |
413 | too_large | JSON body over 1 MB, or APK upload over the 1 GB cap. |
422 | not_interactive, no_launcher_activity, install failures | The request was valid but the action failed on the device. |
503 | device_offline | The phone isn't reachable right now (e.g. rebooting). Retry shortly. |
504 | adb_timeout | The device took too long to respond. |
Limits & caveats
UI actions on a device run in order, so a snapshot's refs stay valid for the next tap. Different devices run in parallel. Refs are shared by everyone driving the same device.
Reading the accessibility tree takes roughly 1–3 s. Don't poll in a tight loop — use /wait, which checks every 500 ms for you.
/type sends printable ASCII and \n. Anything else returns 400 unsupported_text. The literal sequence %s is typed as a space.
WebViews, Flutter apps and games often show little or nothing in the snapshot. Use /screenshot plus {"x", "y"} taps there.
Account sign-ins (Google and others) are done once by a person, by design. The agent works inside the session you set up and never handles your passwords or 2FA.
The phones aren't Play Protect certified, so some banking, DRM-streaming and game apps won't run. We'll tell you plainly when an app is one of them.
Reference · Account & devices
Which account this key belongs to, and the ids of the devices it controls. A good first call to check your key works.
curl -s -H "X-API-Key: $ERIUS_KEY" \
$ERIUS_API/whoami{
"tenant": "your-team",
"keyId": "k1",
"devices": ["dev_7f3a"]
}Your devices with live status: whether the phone is online and booted, its Android version and screen size. Only your own devices are listed.
curl -s -H "X-API-Key: $ERIUS_KEY" \
$ERIUS_API/devices{
"devices": [
{
"id": "dev_7f3a",
"tenant": "your-team",
"state": "device",
"bootCompleted": true,
"android": "13",
"sdk": 33,
"screen": { "width": 720, "height": 1280 }
}
]
}state is "device" when the phone is online. Entries may also carry internal fields (such as serial); don't build on those.
Same as one entry of /devices, plus foreground: the app and window currently on screen (see /foreground).
Reference · Reading the screen
A fresh screenshot of the phone as image/png, 720×1280. Accepts GET or POST.
curl -s -H "X-API-Key: $ERIUS_KEY" \
-o shot.png \
$ERIUS_API/device/dev_7f3a/screenshotContent-Type: image/png
Cache-Control: no-store
<PNG bytes · 720×1280>The accessibility tree of the current screen, with [ref=N] on every element you can act on, plus what's in the foreground. Add ?format=text to get just the tree as plain text.
| Param | Description |
|---|---|
format | text → plain-text tree only. Default: JSON. |
maxDepth, maxNodes | Optional numbers to trim very large trees. |
curl -s -H "X-API-Key: $ERIUS_KEY" \
$ERIUS_API/device/dev_7f3a/snapshot{
"foreground": {
"package": "org.fdroid.fdroid",
"activity": "org.fdroid.IconActivity",
"window": "org.fdroid.fdroid/org.fdroid.IconActivity",
"crashDialog": false,
"crashedPackage": null
},
"snapshot": "- ScrollView [ref=1]\n - View\n - View [ref=2]\n - Text \"New apps\"\n ..."
}- ScrollView [ref=1]
- View
- View [ref=2]
- Text "New apps"
- View [ref=3]
- View [ref=4]
- View [ref=5]
- Text "ADB Captain"
- View [ref=6]
- Text "Starling"
- View
- Text "F-Droid"
- View [ref=18]
- View (Repositories)
- View [ref=19]
- View (Settings)
Quoted strings are visible text; parentheses are content descriptions (labels on icons). Refs are only valid until the next snapshot.
What's on screen right now, without reading the whole tree. crashDialog is true when Android is showing an "app has stopped" or "isn't responding" dialog, with the crashed package in crashedPackage.
curl -s -H "X-API-Key: $ERIUS_KEY" \
$ERIUS_API/device/dev_7f3a/foreground{
"package": "com.android.settings",
"activity": "com.android.settings.Settings",
"window": "com.android.settings/com.android.settings.Settings",
"crashDialog": false,
"crashedPackage": null
}Block until the given text appears on screen, then return the snapshot. timeout is in milliseconds (default 10000, max 120000); on timeout you get 408 WaitTimeout.
curl -s -H "X-API-Key: $ERIUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"text": "Recently updated", "timeout": 15000}' \
$ERIUS_API/device/dev_7f3a/wait{
"ok": true,
"snapshot": "- ScrollView [ref=1]\n ..."
}Reference · Input
All input endpoints return {"ok": true} on success.
Tap an element or a point. Pass exactly one way of choosing what to tap.
| Body | Taps |
|---|---|
{"ref": 12} | Element with that ref in the latest snapshot. |
{"text": "Search"} | Best match on visible text: exact, then case-insensitive, then substring. |
{"contentDesc": "Settings"} | Best match on an element's content description (icon label). |
{"target": {"text": "…"}} | Same as above, nested form. |
{"x": 360, "y": 640} | A point in screen pixels. |
curl -s -H "X-API-Key: $ERIUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"text": "Search settings"}' \
$ERIUS_API/device/dev_7f3a/tap{ "ok": true }
// nothing matched
{
"ok": false,
"error": "SelectorNotFound",
"message": "nothing on screen matches {\"text\":\"Serch\"}"
}Same targets as /tap. With coordinates, ms sets how long to hold (default 800).
{ "x": 360, "y": 900, "ms": 1200 }Type into the focused field — or tap a target first, then type. clear empties the field before typing; enter presses Enter afterwards. ASCII only.
| Field | Description |
|---|---|
text | Required. Printable ASCII; \n becomes Enter. |
ref / target | Optional element to tap before typing. |
clear, enter | Optional booleans. |
curl -s -H "X-API-Key: $ERIUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"text": "battery saver",
"target": {"text": "Search settings"},
"clear": true, "enter": true}' \
$ERIUS_API/device/dev_7f3a/type{ "ok": true }
// non-ASCII text
{
"ok": false,
"error": "unsupported_text",
"message": "only printable ASCII and \\n can be typed through adb input"
}Press a hardware or system key. key is a name — back home enter delete tab escape up down left right space power volup voldown recent menu wakeup sleep search — or a numeric Android keycode.
curl -s -H "X-API-Key: $ERIUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"key": "back"}' \
$ERIUS_API/device/dev_7f3a/press{ "ok": true }Swipe from one point to another in screen pixels. ms is the gesture duration (default 300).
curl -s -H "X-API-Key: $ERIUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"x1": 360, "y1": 1000,
"x2": 360, "y2": 300, "ms": 400}' \
$ERIUS_API/device/dev_7f3a/swipe{ "ok": true }Scroll a specific scrollable element, chosen by ref, target or text. direction is up, down, left or right.
{ "ref": 1, "direction": "down" }Reference · Apps
Open an installed app by package name (optionally a specific activity), or open a URL — http, https or market — optionally in a given package. Returns how the launch went and what ended up in the foreground.
curl -s -H "X-API-Key: $ERIUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"package": "org.fdroid.fdroid"}' \
$ERIUS_API/device/dev_7f3a/launch
# or open a link
-d '{"url": "https://example.com"}'{
"ok": true,
"component": "org.fdroid.fdroid/org.fdroid.IconActivity",
"launchState": "COLD",
"totalTimeMs": 262,
"foreground": {
"package": "org.fdroid.fdroid",
"activity": "org.fdroid.IconActivity",
"window": "org.fdroid.fdroid/org.fdroid.IconActivity",
"crashDialog": false,
"crashedPackage": null
}
}Errors: 404 not_installed if the package isn't on the phone; 422 no_launcher_activity if it has no launcher entry (pass activity).
Start an Android intent directly, for deep links and app-specific entry points. extras values are sent as boolean, integer or string extras according to their JSON type. Returns {ok, output, foreground}.
{
"action": "android.intent.action.VIEW",
"data": "myapp://orders/42",
"package": "com.example.myapp",
"extras": { "from_test": true, "retry": 2 }
}Install an APK on the phone. Upload it as the raw request body or as a multipart file field; reinstalls over an existing version. The API reads the APK first and returns its package details along with the result.
| Option | Description |
|---|---|
| raw body | --data-binary @app.apk |
| multipart | -F apk=@app.apk |
?grant=1 | Grant all runtime permissions at install. |
?downgrade=1 | Allow installing an older version over a newer one. |
curl -s -H "X-API-Key: $ERIUS_KEY" \
-F apk=@F-Droid.apk \
"$ERIUS_API/device/dev_7f3a/install?grant=1"{
"ok": true,
"device": "dev_7f3a",
"package": "org.fdroid.fdroid",
"versionCode": "2000051",
"versionName": "2.0.1",
"label": "F-Droid",
"launchActivity": "org.fdroid.MainActivity",
"minSdk": 24,
"targetSdk": 37,
"abis": ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"],
"adbOutput": "Performing Streamed Install\nSuccess",
"ms": 957
}Uploads up to 1 GB. A file that isn't an APK returns 400 not_an_apk; an install Android rejects returns 422 with Android's reason in error (e.g. INSTALL_FAILED_…). ARM-only APKs install too.
Installed packages. ?thirdParty=1 limits the list to apps that were installed on top of the system — usually the ones you care about.
curl -s -H "X-API-Key: $ERIUS_KEY" \
"$ERIUS_API/device/dev_7f3a/apps?thirdParty=1"{
"count": 2,
"packages": [
"org.cromite.cromite",
"org.fdroid.fdroid"
]
}Without thirdParty=1 the response is {count, thirdPartyCount, packages: [{package, thirdParty}]} for every package on the phone.
Uninstall a package.
curl -s -H "X-API-Key: $ERIUS_KEY" -X DELETE \
$ERIUS_API/device/dev_7f3a/apps/org.fdroid.fdroid{
"ok": true,
"package": "org.fdroid.fdroid",
"output": "Success"
}Force-stop an app, e.g. to start the next test from a cold launch. Body: {"package": "org.fdroid.fdroid"} → {"ok": true}.
Recent lines from Android's crash log — Java FATAL EXCEPTIONs and native crashes — so your agent can tell a crash from a slow screen. ?lines= sets how many (default 200, max 5000).
curl -s -H "X-API-Key: $ERIUS_KEY" \
"$ERIUS_API/device/dev_7f3a/crashes?lines=50"{
"lines": [
"E AndroidRuntime: FATAL EXCEPTION: main",
"E AndroidRuntime: Process: com.example.myapp, PID: 4121",
"E AndroidRuntime: java.lang.IllegalStateException: …",
"…"
]
}Example lines are illustrative of the format; an empty lines array means no crashes were logged.
GET /healthz needs no key and returns {"ok": true}; use it to check the API is reachable.
API keys are issued by hand during early access, together with your phone.
Request a device