---
title: Phone endpoints
description: Every /v1/phones endpoint for cloud phones, apps, the live screen, direct control and agent runs, with parameters, limits, responses and errors.
layout: reference
---

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](/docs/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](/docs/phone#pricing).

For a walkthrough, start with the [cloud phone overview](/docs/phone). 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

:::params Attributes
- `id` string
  `ph_` followed by 20 characters.
- `object` string
  Always `phone`.
- `status` string
  `starting` (about a minute, longer while startup apps install), `ready`, `failed` or `deleted`.
- `name` string | null
  Your label.
- `user` string | null
  Your own end user's id, if you set one.
- `metadata` object | null
  What you stored with it.
- `startup_apps` array
  One entry per app you listed in `apps`: `source`, `status` (`pending`, `installed` or `failed`), `packages` (the package names it installed) and `error`.
- `created_at`, `ready_at`, `last_active_at`, `deleted_at` integer | null
  Unix seconds.
- `deletes_after_idle_at` integer | null
  When the phone is deleted unless it's used before then (1 hour after its last activity).
- `awake_seconds` integer
  Awake time billed so far.
- `charge_usd` number
  What the phone has cost so far.
- `end_reason` string | null
  Why it was deleted: `deleted` (by you), `idle`, `balance` or `failed`.
- `error` string | null
  Why it failed to start.
:::

## Create a phone

::endpoint POST /v1/phones

Creates a phone and returns `201` with the phone object while it starts. Follow it with [Retrieve a phone](#retrieve-a-phone) and `wait`. You need 15 minutes of phone time on your balance. Each account can have 3 phones at a time.

:::params Body parameters
- `apps` array of strings
  Up to 20 apps to install before the phone is `ready`: `https://` links to `.apk`, `.apks` or `.xapk` files, or names of [uploaded APKs](#upload-an-apk). A failed install doesn't fail the phone; check `startup_apps`.
- `name` string
  A label, up to 80 characters.
- `user` string
  Your end user's id (up to 200 characters), to list their phones with `GET /v1/phones?user=`.
- `metadata` object
  Up to 4 KB of your own data.
- `wait` integer
  Seconds to hold the request until the phone is ready, up to 50.
:::

```bash title="cURL" aside
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}'
```

:::aside
```json title="Response"
{
  "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

::endpoint GET /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

::endpoint GET /v1/phones

Returns `{"object": "list", "data": [phone, …]}`, newest first. `user=` keeps one end user's phones; `all=1` includes deleted ones.

## Delete a phone

::endpoint DELETE /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

::endpoint GET /v1/phones/{id}/screenshot

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

```bash title="cURL" aside
curl https://lovelyrouter.com/v1/phones/ph_2758eedebf08496f87e0/screenshot \
  -H "Authorization: Bearer $LOVELY_API_KEY" -o screen.webp
```

## List elements

::endpoint GET /v1/phones/{id}/elements

What'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

::endpoint POST /v1/phones/{id}/actions

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

:::params 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`.
:::

```bash title="cURL" aside
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

::endpoint POST /v1/phones/{id}/screen

Returns `{"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.

:::params Body parameters
- `ttl_seconds` integer, default `3600`
  How long the link works: 60 to 86,400. A screen already open keeps working.
- `view_only` boolean
  Opens the page in view-only mode. It is a display setting, not a permission: a viewer can turn it off.
:::

## Install an app

::endpoint POST /v1/phones/{id}/apps

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

:::params Body parameters
- `url` string
  An `https://` link to an `.apk`, `.apks` or `.xapk` file.
- `apk` string
  Or the name of an [uploaded APK](#upload-an-apk).
:::

`GET /v1/phones/{id}/apps` lists the installed apps' package names, and `DELETE /v1/phones/{id}/apps/{package}` uninstalls one.

## Upload an APK

::endpoint PUT /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](#upload-a-large-apk). `.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](#storage).

```bash title="cURL" aside
curl -X PUT https://lovelyrouter.com/v1/phones/apks/myapp.apk \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  --data-binary @myapp.apk
```

`GET /v1/phones/apks` lists them, and `DELETE /v1/phones/apks/{name}` deletes one.

## Upload a large APK

::endpoint POST /v1/phones/apks/{name}/uploads

For 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](#upload-an-apk): it's stored under `name` and replaces a file of that name.

:::params Body parameters
- `bytes` integer, required
  The 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:

1. `PUT /v1/phones/apks/{name}/uploads/{id}/{n}` for each part `n` from 1 to `parts`, with the file's bytes from `(n-1) × part_size` as the body. Every part is `part_size` bytes (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.
2. `POST /v1/phones/apks/{name}/uploads/{id}/complete` joins the parts and returns `201` with `{name, bytes, sha256}`, or `409` naming 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.

```bash title="cURL" aside
# 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"
```

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

## The run object

:::params Attributes
- `id` string
  `prun_` followed by 20 characters.
- `object` string
  Always `phone.run`.
- `phone_id` string
  The phone it runs on.
- `status` string
  `queued` (the phone is starting), `running`, `waiting` (it asked you something), `succeeded`, `failed` or `stopped` (by you, or on reaching `max_steps`).
- `goal`, `model`, `max_steps` string, string, integer
  What you sent.
- `steps` integer
  Steps so far.
- `question` string | null
  While `waiting`: what the agent needs from you.
- `answer` string | null
  The result, when it's done.
- `error` string | null
  Why it failed or stopped.
- `llm_usd` number
  What the model's tokens cost (set when the run ends).
- `trace` array
  With `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

::endpoint POST /v1/phones/runs

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

:::params Body parameters
- `goal` string, required
  What to do, in plain words, with every value the task needs. Up to 10,000 characters.
- `phone_id` string
  Run on this phone instead of a new one. `POST /v1/phones/{id}/runs` does the same.
- `apps`, `name`, `user`, `metadata`
  For the new phone, as in [Create a phone](#create-a-phone).
- `mode` string, default `smart`
  `smart`: 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](/docs/phone#fast-mode).
- `plan` boolean, default `true`
  Fast 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`.
- `model` string, default `gemini-3.8-flash`
  Any chat model from `GET /v1/models`. Its tokens are billed at the normal price.
- `max_steps` integer, default `40`
  1 to 200. A run that reaches it ends as `stopped` with `error` `reached the step limit (N)`.
- `wait` integer
  Seconds to hold the request until the run ends or asks something, up to 50.
:::

## Retrieve a run

::endpoint GET /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

::endpoint POST /v1/phones/runs/{id}/reply

Answers the run's `question` with `{"answer": "…"}`; the run continues. Returns `409` when the run isn't waiting.

## Stop a run

::endpoint POST /v1/phones/runs/{id}/stop

Stops the run after its current step. The phone stays.

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