---
title: Cloud phone
description: Real Android 14 phones in the cloud for your agents. Install any APK, let our phone agent work a goal in plain words, or drive the phone yourself with taps, typing and screenshots.
---

A cloud phone is an Android 14 phone that runs in the cloud and that your code controls through the API. Use it when an app has no API: the phone runs the real app, and the agent or your own code works it like a person would.

You can use a phone in two ways:

- **Phone agent.** Send a goal in plain words, such as "Open the app, search for Termux and tell me the first result". The agent reads the screen, taps, types and scrolls until the goal is done, then returns an `answer`.
- **Direct control.** Your own code or agent takes screenshots, reads the elements on screen and sends taps, swipes, typing and key presses.

A person can watch the screen live, or take it over, from any browser.

Phones use the same API key and prepaid balance as the rest of Lovely Router. A phone costs $1.20 per hour while it's awake, billed per minute. Models used by the agent are billed at their normal API price on top.

## Run your first goal

:::steps
### Get an API key

Create a key in [API keys](/app/keys) and export it.

```bash
export LOVELY_API_KEY="lr-..."
```

### Start a run on a new phone

`POST /v1/phones/runs` creates a phone, installs the apps you list and starts the agent on your goal. It answers at once with status `202` and the run's `id`. A new phone takes about a minute to start, and longer if it installs apps first.

```bash title="cURL"
curl https://lovelyrouter.com/v1/phones/runs \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "goal": "Open the F-Droid app, search for Termux, and tell me the one-line summary of the first result.",
    "apps": ["https://f-droid.org/F-Droid.apk"]
  }'
```

```python title="Python"
import os, requests

API = "https://lovelyrouter.com/v1/phones"
H = {"Authorization": f"Bearer {os.environ['LOVELY_API_KEY']}"}

run = requests.post(f"{API}/runs", headers=H, json={
    "goal": "Open the F-Droid app, search for Termux, and tell me the one-line summary of the first result.",
    "apps": ["https://f-droid.org/F-Droid.apk"],
}).json()
print(run["id"], run["phone_id"])
```

```js title="Node.js"
const API = 'https://lovelyrouter.com/v1/phones';
const H = { Authorization: `Bearer ${process.env.LOVELY_API_KEY}`, 'content-type': 'application/json' };

const run = await (await fetch(`${API}/runs`, {
  method: 'POST',
  headers: H,
  body: JSON.stringify({
    goal: 'Open the F-Droid app, search for Termux, and tell me the one-line summary of the first result.',
    apps: ['https://f-droid.org/F-Droid.apk'],
  }),
})).json();
console.log(run.id, run.phone_id);
```

### Wait for the answer

`GET /v1/phones/runs/{id}?wait=50` holds the request for up to 50 seconds and answers early when the run finishes or asks you something. Repeat it until `status` is `succeeded`, `failed`, `stopped` or `waiting`. Add `trace=1` to get the agent's steps.

```bash title="cURL"
curl "https://lovelyrouter.com/v1/phones/runs/prun_...?wait=50&trace=1" \
  -H "Authorization: Bearer $LOVELY_API_KEY"
```

```python title="Python"
import time

while run["status"] in ("queued", "running"):
    run = requests.get(f"{API}/runs/{run['id']}", headers=H, params={"wait": 50}).json()
print(run["status"], run["answer"])
```

```js title="Node.js"
let r = run;
while (r.status === 'queued' || r.status === 'running') {
  r = await (await fetch(`${API}/runs/${r.id}?wait=50`, { headers: H })).json();
}
console.log(r.status, r.answer);
```

```json
{
  "id": "prun_7ae48d4cb099421dbd2c",
  "object": "phone.run",
  "phone_id": "ph_2758eedebf08496f87e0",
  "status": "succeeded",
  "steps": 6,
  "answer": "The first result is \"Termux Hub\": \"A lightweight Android app that indexes Termux tools from metadata.\"",
  "llm_usd": 0.0203
}
```
:::

The phone stays yours after the run. Send the next goal to the same phone with `POST /v1/phones/{phone_id}/runs`, or pass `"phone_id"` to `POST /v1/phones/runs`. The apps and logins from earlier runs are still there.

## What a phone costs {#pricing}

| | Price |
| - | - |
| Phone, while awake | $1.20 per hour, billed per minute |
| Phone, while asleep | Free |
| Uploaded APKs | The first 1 GB free, then $0.30 per GB a month, billed hourly |
| Agent runs | The model's normal API price per token (default `gemini-3.8-flash`, about $0.003 a step) |

A phone falls asleep after 5 minutes without use and wakes in about 2 seconds when you use it again, with the screen just as you left it. A phone that hasn't been used for **1 hour** is deleted, along with its apps and data. You can delete it yourself any time with `DELETE /v1/phones/{id}`.

Starting a phone needs 15 minutes of phone time on your balance. When the balance runs out, runs stop and the phone falls asleep. Each account can have 3 phones at a time.

## Apps

The phone has Chrome, Settings and the other standard Android apps, plus Google Play services. It has no Play Store, so you install apps from APK files. Any APK works, and so do split bundles (`.apks`, `.xapk`). An `.xapk`'s game data (OBB files) is copied to the phone too.

- **When the phone starts:** list the apps in `apps` when you create the phone or start a run. Each entry is an `https://` link to the file, or the name of an APK you uploaded. The phone's `startup_apps` field shows how each install went.
- **Later:** `POST /v1/phones/{id}/apps` with `{"url": "https://…"}` or `{"apk": "myapp.apk"}`. The request returns when the install is done.
- **Your own APKs:** upload a file once and install it by name on any phone. Up to 100 MB, send it in one request with `PUT /v1/phones/apks/{name}`. Up to 500 MB, upload it in 50 MB parts with `POST /v1/phones/apks/{name}/uploads` ([how](/docs/api/phones#upload-a-large-apk)). For a larger file, host it yourself and install it by URL. Your first 1 GB of uploads is stored free; past that, storage costs $0.30 per GB a month.

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

# Install on a phone
curl https://lovelyrouter.com/v1/phones/ph_.../apps \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"apk": "myapp.apk"}'
```

```python title="Python (a file over 100 MB, in parts)"
import os, requests

path, name = "game.xapk", "game.xapk"
up = requests.post(f"{API}/apks/{name}/uploads", headers=H, json={"bytes": os.path.getsize(path)}).json()
with open(path, "rb") as f:
    for n in range(1, up["parts"] + 1):
        requests.put(f"{API}/apks/{name}/uploads/{up['id']}/{n}", headers=H, data=f.read(up["part_size"])).raise_for_status()
print(requests.post(f"{API}/apks/{name}/uploads/{up['id']}/complete", headers=H).json())
```

## Fast mode {#fast-mode}

When speed matters more than careful reasoning, start the run with `"mode": "fast"`. A fast decision model reads the screen's elements and picks most steps in well under a second, instead of the few seconds a chat model takes. The text it types is taken from your goal and your replies. It also sees the screenshot, so it can tap things that aren't buttons (game pieces, pictures) and swipe. It acts on its best guess even when unsure, because in a game any move beats waiting. The chat model (`model`) only writes the final answer and any question for you, so a fast run also costs less in model tokens. The fast model itself is included in the phone's price.

Beside the fast model, a coach model (included in the phone's price) works alongside it: it writes a short strategy for the goal (such as which moves to prefer in a game) and a note on the situation now, and rewrites them every few steps or when the run looks stuck. The fast model never waits for it; every step uses the latest version (`"plan": false` turns the coach off).

A move that leaves the screen exactly as it was isn't tried again on that screen, and the coach is told at once. In a game, the agent doesn't press New Game, Restart or Try again unless the game is over or your goal asks for it.

Fast mode suits games and short, clear goals: dismissing dialogs, opening an app and searching, tapping through a known flow. For goals that need judgement, use the default `smart` mode, or give the fast agent the quick sub-tasks of a bigger job.

```bash title="cURL"
curl https://lovelyrouter.com/v1/phones/ph_.../runs \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"goal": "Open F-Droid and search for Termux", "mode": "fast"}'
```

## When the agent needs you

If the agent needs something only you know or should decide (a code sent to your phone number, a password, a choice), the run's status becomes `waiting` and `question` says what it needs. Answer with `POST /v1/phones/runs/{id}/reply` and `{"answer": "…"}`, and the run continues. You can also open the live screen, do the step yourself, then reply `done`.

The agent never pays, sends money, deletes data or posts publicly unless the goal says so. Stop a run any time with `POST /v1/phones/runs/{id}/stop`.

## Watch the screen live {#live-screen}

`POST /v1/phones/{id}/screen` returns a link to the phone's live screen. It opens in any browser, needs no plugin and can be embedded in an iframe on your own page. Whoever has the link can see the screen **and** tap and type on it, so share it only with the person who should use the phone. Links last for one hour by default (`ttl_seconds`, up to 24 hours). The screen takes the phone's shape: when an app turns it to landscape, or you send `rotate`, it turns too.

## Drive the phone yourself {#direct-control}

Your own code or agent can control the phone directly. Coordinates are in pixels of the screenshot, whose short side is 540 pixels (540×1200 upright, 1200×540 in landscape).

- `GET /v1/phones/{id}/screenshot` returns the screen as a WebP image.
- `GET /v1/phones/{id}/elements` lists what's on screen (buttons, text, fields), each with its center point, size and flags such as `clickable` and `editable`.
- `POST /v1/phones/{id}/actions` sends an action: `tap`, `long_press`, `swipe`, `scroll`, `type` (any language), `key` (`back`, `home`, `enter`…), `open_url`, `launch` (by package name), `stop_app`, `wait`, `rotate` (`portrait` or `landscape`), or `shell` (an `adb shell` command on the phone). Add `"screenshot": true` to get the screen after the action.

```bash title="cURL"
curl https://lovelyrouter.com/v1/phones/ph_.../actions \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"action": "tap", "x": 270, "y": 1116, "screenshot": true}'
```

Every field and error is in the [API reference](/docs/api/phones).
