Docs · Device API

Your agent's phone, over plain HTTP.

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.

Early access Auth: X-API-Key JSON in · JSON or PNG out Android 13 · 720×1280

Overview

What the API does.

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.

Devices

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.

Snapshots & refs

/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.

Selectors

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.

Coordinates

For anything the tree can't see (WebViews, Flutter, games), take a /screenshot and tap with {"x", "y"} in screen pixels (720×1280).

Authentication

One key, your devices only.

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

Look, then tap.

The basic agent loop in five calls: find your phone, see the screen, tap something, check what happened.

shell · curlquickstart
# 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

One error shape, real status codes.

every error responsejson
HTTP/1.1 403 Forbidden
{
  "ok": false,
  "error": "forbidden",
  "message": "device not found or not accessible with this key"
}
StatusCodes you'll seeMeaning
400bad_request, bad_json, unsupported_text, not_an_apkBad input — the message says which field.
401unauthorizedMissing or wrong API key.
403forbiddenDevice isn't yours, or doesn't exist (deliberately the same).
404SelectorNotFound, ElementNotFound, not_installed, not_foundNothing on screen matched, unknown ref, package not installed, unknown route.
408WaitTimeout/wait didn't see the text in time.
413too_largeJSON body over 1 MB, or APK upload over the 1 GB cap.
422not_interactive, no_launcher_activity, install failuresThe request was valid but the action failed on the device.
503device_offlineThe phone isn't reachable right now (e.g. rebooting). Retry shortly.
504adb_timeoutThe device took too long to respond.

Limits & caveats

Worth knowing before you build.

One action at a time per phone

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.

Snapshots take a second or three

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.

Typing is ASCII-only

/type sends printable ASCII and \n. Anything else returns 400 unsupported_text. The literal sequence %s is typed as a space.

The tree can't see everything

WebViews, Flutter apps and games often show little or nothing in the snapshot. Use /screenshot plus {"x", "y"} taps there.

Sign-ins are a human step

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.

Some apps refuse virtual devices

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

Find your phones.

GET/whoami

Which account this key belongs to, and the ids of the devices it controls. A good first call to check your key works.

request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  $ERIUS_API/whoami
response200
{
  "tenant": "your-team",
  "keyId": "k1",
  "devices": ["dev_7f3a"]
}
GET/devices

Your devices with live status: whether the phone is online and booted, its Android version and screen size. Only your own devices are listed.

request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  $ERIUS_API/devices
response200
{
  "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.

GET/device/:id

Same as one entry of /devices, plus foreground: the app and window currently on screen (see /foreground).

Reference · Reading the screen

See what the user would see.

GETPOST/device/:id/screenshot

A fresh screenshot of the phone as image/png, 720×1280. Accepts GET or POST.

request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  -o shot.png \
  $ERIUS_API/device/dev_7f3a/screenshot
response200
Content-Type: image/png
Cache-Control: no-store

<PNG bytes · 720×1280>
GETPOST/device/:id/snapshot

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.

ParamDescription
formattext → plain-text tree only. Default: JSON.
maxDepth, maxNodesOptional numbers to trim very large trees.
request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  $ERIUS_API/device/dev_7f3a/snapshot
response200
{
  "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 ..."
}
snapshot · format=text (trimmed)real device output
- 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.

GET/device/:id/foreground

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.

request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  $ERIUS_API/device/dev_7f3a/foreground
response200
{
  "package": "com.android.settings",
  "activity": "com.android.settings.Settings",
  "window": "com.android.settings/com.android.settings.Settings",
  "crashDialog": false,
  "crashedPackage": null
}
POST/device/:id/wait

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.

request
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
response200
{
  "ok": true,
  "snapshot": "- ScrollView [ref=1]\n ..."
}

Reference · Input

Touch, type, press.

All input endpoints return {"ok": true} on success.

POST/device/:id/tap

Tap an element or a point. Pass exactly one way of choosing what to tap.

BodyTaps
{"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.
request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"text": "Search settings"}' \
  $ERIUS_API/device/dev_7f3a/tap
response200 · 404
{ "ok": true }

// nothing matched
{
  "ok": false,
  "error": "SelectorNotFound",
  "message": "nothing on screen matches {\"text\":\"Serch\"}"
}
POST/device/:id/longpress

Same targets as /tap. With coordinates, ms sets how long to hold (default 800).

request body
{ "x": 360, "y": 900, "ms": 1200 }
POST/device/:id/type

Type into the focused field — or tap a target first, then type. clear empties the field before typing; enter presses Enter afterwards. ASCII only.

FieldDescription
textRequired. Printable ASCII; \n becomes Enter.
ref / targetOptional element to tap before typing.
clear, enterOptional booleans.
request
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
response200 · 400
{ "ok": true }

// non-ASCII text
{
  "ok": false,
  "error": "unsupported_text",
  "message": "only printable ASCII and \\n can be typed through adb input"
}
POST/device/:id/press

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.

request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"key": "back"}' \
  $ERIUS_API/device/dev_7f3a/press
response200
{ "ok": true }
POST/device/:id/swipe

Swipe from one point to another in screen pixels. ms is the gesture duration (default 300).

request
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
response200
{ "ok": true }
POST/device/:id/scroll

Scroll a specific scrollable element, chosen by ref, target or text. direction is up, down, left or right.

request body
{ "ref": 1, "direction": "down" }

Reference · Apps

Install, open, inspect.

POST/device/:id/launch

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.

request
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"}'
response200
{
  "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).

POST/device/:id/intent

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}.

request body
{
  "action": "android.intent.action.VIEW",
  "data": "myapp://orders/42",
  "package": "com.example.myapp",
  "extras": { "from_test": true, "retry": 2 }
}
POST/device/:id/install

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.

OptionDescription
raw body--data-binary @app.apk
multipart-F apk=@app.apk
?grant=1Grant all runtime permissions at install.
?downgrade=1Allow installing an older version over a newer one.
request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  -F apk=@F-Droid.apk \
  "$ERIUS_API/device/dev_7f3a/install?grant=1"
response200
{
  "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.

GET/device/:id/apps

Installed packages. ?thirdParty=1 limits the list to apps that were installed on top of the system — usually the ones you care about.

request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  "$ERIUS_API/device/dev_7f3a/apps?thirdParty=1"
response200
{
  "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.

DELETE/device/:id/apps/:package

Uninstall a package.

request
curl -s -H "X-API-Key: $ERIUS_KEY" -X DELETE \
  $ERIUS_API/device/dev_7f3a/apps/org.fdroid.fdroid
response200
{
  "ok": true,
  "package": "org.fdroid.fdroid",
  "output": "Success"
}
POST/device/:id/stop

Force-stop an app, e.g. to start the next test from a cold launch. Body: {"package": "org.fdroid.fdroid"} → {"ok": true}.

GET/device/:id/crashes

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).

request
curl -s -H "X-API-Key: $ERIUS_KEY" \
  "$ERIUS_API/device/dev_7f3a/crashes?lines=50"
response200
{
  "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.

Want a key?

API keys are issued by hand during early access, together with your phone.

Request a device