Phone endpoints
Every /v1/phones endpoint for cloud phones, apps, the live screen, direct control and agent runs, with parameters, limits, responses and errors.
The cloud phone API lives under https://lovelyrouter.com/v1/phones. Authenticate every request with your API key as Authorization: Bearer $LOVELY_API_KEY (Authentication). Bodies are JSON, except APK uploads and screenshots. A phone costs $1.20 per awake hour, billed per minute from your prepaid balance; see what a phone costs.
For a walkthrough, start with the cloud phone overview. Coordinates everywhere are pixels of the screenshot, whose short side is 540 pixels: 540×1200 upright, 1200×540 when the phone is turned to landscape (a game, a video). The screenshot's width and height say which.
The phone object#
Attributes
idstringph_ followed by 20 characters.
objectstringAlways phone.
statusstringstarting (about a minute, longer while startup apps install), ready, failed or deleted.
namestring | nullYour label.
userstring | nullYour own end user's id, if you set one.
metadataobject | nullWhat you stored with it.
startup_appsarrayOne entry per app you listed in apps: source, status (pending, installed or failed), packages (the package names it installed) and error.
created_atready_atUnix seconds.
deletes_after_idle_atinteger | nullWhen the phone is deleted unless it's used before then (1 hour after its last activity).
awake_secondsintegerAwake time billed so far.
charge_usdnumberWhat the phone has cost so far.
end_reasonstring | nullWhy it was deleted: deleted (by you), idle, balance or failed.
errorstring | nullWhy it failed to start.
Create a phone#
/v1/phonesCreates a phone and returns 201 with the phone object while it starts. Follow it with Retrieve a phone and wait. You need 15 minutes of phone time on your balance. Each account can have 3 phones at a time.
Body parameters
appsarray of stringsUp to 20 apps to install before the phone is ready: https:// links to .apk, .apks or .xapk files, or names of uploaded APKs. A failed install doesn't fail the phone; check startup_apps.
namestringA label, up to 80 characters.
userstringYour end user's id (up to 200 characters), to list their phones with GET /v1/phones?user=.
metadataobjectUp to 4 KB of your own data.
waitintegerSeconds to hold the request until the phone is ready, up to 50.
curl https://lovelyrouter.com/v1/phones \
-H "Authorization: Bearer $LOVELY_API_KEY" \
-H "content-type: application/json" \
-d '{"apps": ["https://f-droid.org/F-Droid.apk"], "wait": 50}'{
"id": "ph_2758eedebf08496f87e0",
"object": "phone",
"status": "starting",
"name": null,
"user": null,
"metadata": null,
"startup_apps": [{ "source": "https://f-droid.org/F-Droid.apk", "status": "pending" }],
"created_at": 1793308400,
"ready_at": null,
"last_active_at": 1793308400,
"deletes_after_idle_at": 1793312000,
"awake_seconds": 0,
"charge_usd": 0,
"deleted_at": null,
"end_reason": null,
"error": null,
"price_per_hour_usd": 1.20
}Retrieve a phone#
/v1/phones/{id}Returns the phone object. With wait (seconds, up to 50), a starting phone holds the request until it's ready.
List phones#
/v1/phonesReturns {"object": "list", "data": [phone, …]}, newest first. user= keeps one end user's phones; all=1 includes deleted ones.
Delete a phone#
/v1/phones/{id}Deletes the phone with its apps and data, stops its run and settles its last minutes. Returns the phone object with status deleted. A phone is also deleted on its own after 1 hour without activity.
Get a screenshot#
/v1/phones/{id}/screenshotReturns the screen now as a WebP image (image/webp, 540 pixels on its short side). With format=json, returns {screenshot, width, height, app, elements} with the image as base64.
curl https://lovelyrouter.com/v1/phones/ph_2758eedebf08496f87e0/screenshot \
-H "Authorization: Bearer $LOVELY_API_KEY" -o screen.webpList elements#
/v1/phones/{id}/elementsWhat's on screen, from Android's view tree: {"app", "activity", "width", "height", "data": [element, …]}. Each element has id, type (Button, EditText, TextView…), text, desc (the accessibility label), resource (the view id), x and y (its center), w and h, and flags (clickable, long-clickable, scrollable, editable, checked/unchecked, focused, selected, password, disabled). Screens drawn without views (games, some canvases) list few elements: use the screenshot.
Send an action#
/v1/phones/{id}/actionsDoes one thing on the phone and returns {"ok": true} (or ok: false with error). Add "screenshot": true to get screenshot (base64 WebP) after the screen settles. Returns 409 while an agent run is driving the phone.
Actions
{"action": "tap", "x", "y"}Tap a point.
{"action": "long_press", "x", "y", "ms"?}Press and hold (default 800 ms).
{"action": "swipe", "x", "y", "to_x", "to_y", "ms"?}Drag from one point to another (default 300 ms).
{"action": "scroll", "direction", "x"?, "y"?}up, down, left or right; down shows what is below. At a point, or the middle of the screen.
{"action": "type", "text", "clear"?, "submit"?}Type into the focused field, in any language. clear empties it first; submit presses Enter after.
{"action": "key", "key"}back, home, enter, recents, delete, tab, escape, search, menu, power, volume_up, volume_down or wakeup.
{"action": "open_url", "url"}Open a link: a web page in Chrome, or an app link (market://, an app's own scheme).
{"action": "launch", "package"}Open an installed app, e.g. org.fdroid.fdroid.
{"action": "stop_app", "package"}Force-stop an app.
{"action": "wait", "seconds"}Up to 60 seconds.
{"action": "rotate", "orientation"}Turn the phone: portrait or landscape. An app locked to one orientation (many games) keeps it. The live screen follows.
{"action": "shell", "command"}An adb shell command on the phone; the answer has output.
curl https://lovelyrouter.com/v1/phones/ph_2758eedebf08496f87e0/actions \
-H "Authorization: Bearer $LOVELY_API_KEY" \
-H "content-type: application/json" \
-d '{"action": "type", "text": "天气 Tokyo", "submit": true}'Get a live screen link#
/v1/phones/{id}/screenReturns {"url", "expires_at"}: a page with the phone's live screen. Anyone with the link can watch and control the phone with mouse and keyboard, so share it only with the person meant to use it. It can be embedded in an iframe.
Body parameters
ttl_secondsintegerDefault 3600How long the link works: 60 to 86,400. A screen already open keeps working.
view_onlybooleanOpens the page in view-only mode. It is a display setting, not a permission: a viewer can turn it off.
Install an app#
/v1/phones/{id}/appsDownloads and installs an app, and returns when it's done: {"ok", "packages", "error"} (422 when the install failed). Installing an app that is already there updates it.
Body parameters
urlstringAn https:// link to an .apk, .apks or .xapk file.
apkstringOr the name of an uploaded APK.
GET /v1/phones/{id}/apps lists the installed apps' package names, and DELETE /v1/phones/{id}/apps/{package} uninstalls one.
Upload an APK#
/v1/phones/apks/{name}Stores an APK, so you can install it on any phone by name (letters, digits, _, ., -; up to 80 characters). Send the file as the request body, up to 100 MB; for a bigger file (up to 500 MB) use parts. .apk, .apks and .xapk files all work; an .xapk's game data (OBB files) is copied to the phone when it installs. Uploading to the same name replaces the file. Returns 201 with {name, bytes, sha256}. Each account can keep 50, and storage past 1 GB is billed.
GET /v1/phones/apks lists them, and DELETE /v1/phones/apks/{name} deletes one.
curl -X PUT https://lovelyrouter.com/v1/phones/apks/myapp.apk \
-H "Authorization: Bearer $LOVELY_API_KEY" \
--data-binary @myapp.apkUpload a large APK#
/v1/phones/apks/{name}/uploadsFor a file over 100 MB, up to 500 MB, upload it in parts. Start with the file's size, send each part, then finish. The result is the same as Upload an APK: it's stored under name and replaces a file of that name.
Body parameters
bytesintegerRequiredThe file's size in bytes, up to 524,288,000 (500 MB).
It returns 201 with {id, part_size, parts, parts_done, expires_at}. Then:
PUT /v1/phones/apks/{name}/uploads/{id}/{n}for each partnfrom 1 toparts, with the file's bytes from(n-1) × part_sizeas the body. Every part ispart_sizebytes (50 MB) except the last, which is the rest. Parts can go in any order or at the same time, and a part sent again replaces the earlier one.POST /v1/phones/apks/{name}/uploads/{id}/completejoins the parts and returns201with{name, bytes, sha256}, or409naming the parts still missing.
GET /v1/phones/apks/{name}/uploads/{id} shows which parts arrived (parts_done), and DELETE on it gives the upload up. An upload that isn't finished within 24 hours is discarded.
Storage#
Your first 1 GB of uploaded APKs is stored free. Past that, storage costs $0.30 per GB a month, taken from your balance every hour. Delete APKs you no longer need with DELETE /v1/phones/apks/{name}. Uploads need a balance above zero.
# 120 MB file: 3 parts
curl https://lovelyrouter.com/v1/phones/apks/game.xapk/uploads \
-H "Authorization: Bearer $LOVELY_API_KEY" \
-H "content-type: application/json" \
-d "{\"bytes\": $(stat -c %s game.xapk)}"
split -b 50M -d -a 2 game.xapk part.
for n in 1 2 3; do
curl -X PUT https://lovelyrouter.com/v1/phones/apks/game.xapk/uploads/upl_.../$n \
-H "Authorization: Bearer $LOVELY_API_KEY" \
--data-binary @part.0$((n-1))
done
curl -X POST https://lovelyrouter.com/v1/phones/apks/game.xapk/uploads/upl_.../complete \
-H "Authorization: Bearer $LOVELY_API_KEY"The run object#
Attributes
idstringprun_ followed by 20 characters.
objectstringAlways phone.run.
phone_idstringThe phone it runs on.
statusstringqueued (the phone is starting), running, waiting (it asked you something), succeeded, failed or stopped (by you, or on reaching max_steps).
goalmodelWhat you sent.
stepsintegerSteps so far.
questionstring | nullWhile waiting: what the agent needs from you.
answerstring | nullThe result, when it's done.
errorstring | nullWhy it failed or stopped.
llm_usdnumberWhat the model's tokens cost (set when the run ends).
tracearrayWith trace=1: each step's step, note (what the agent saw and why it acted), action, args, app, ok, error and ms, plus unchanged: true when the action left the screen exactly as it was.
Start a run#
/v1/phones/runsCreates a phone and starts the agent on it, or starts it on one of your phones with phone_id. Returns 202 with the run object. A phone runs one goal at a time: 409 while another run is active on it.
Body parameters
goalstringRequiredWhat to do, in plain words, with every value the task needs. Up to 10,000 characters.
phone_idstringRun on this phone instead of a new one. POST /v1/phones/{id}/runs does the same.
appsnameFor the new phone, as in Create a phone.
modestringDefault smartsmart: the model decides every step (a few seconds each). fast: a fast decision model picks nearly every step in well under a second (it always acts, which suits games), and model only writes the answer or a question for you. See Fast mode.
planbooleanDefault trueFast mode only: a coach model runs beside the fast model (included in the phone's price). It writes a short strategy for the goal (for a game, which moves to prefer) and a note on the situation now, and rewrites both every few steps or when the run looks stuck. The fast model never waits for it: each step uses the latest version. The run shows them in strategy, situation and sidecar_updates.
modelstringDefault gemini-3.8-flashAny chat model from GET /v1/models. Its tokens are billed at the normal price.
max_stepsintegerDefault 401 to 200. A run that reaches it ends as stopped with error reached the step limit (N).
waitintegerSeconds to hold the request until the run ends or asks something, up to 50.
Retrieve a run#
/v1/phones/runs/{id}Returns the run object. wait (up to 50 seconds) holds the request until the run ends or asks you something; trace=1 adds the steps, and after_step=N also answers as soon as the run has more than N steps. GET /v1/phones/runs?phone_id= lists runs, newest first.
Reply to a run#
/v1/phones/runs/{id}/replyAnswers the run's question with {"answer": "…"}; the run continues. Returns 409 when the run isn't waiting.
Errors#
| Status | When | | - | - | | 400 | A field is missing or wrong; the message says which. | | 402 | The balance is too low to start a phone, or used up. | | 404 | No such phone, run or APK on your account. | | 409 | The phone is still starting, or busy with a run. | | 410 | The phone was deleted. | | 413 | The APK is too big for one request (over 100 MB: upload it in parts) or over 500 MB. | | 422 | The app didn't install; error says why. | | 429 | You already have 3 phones, or 50 uploaded APKs. | | 503 | All phones are in use right now; try again in a few minutes. |