> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lusor.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Direct control

> Drive a phone yourself: read the screen, tap, type, swipe, press keys and launch apps.

Direct control lets your own code, or your own AI agent, operate a phone one action at a time. Each action runs once, as sent, and returns as soon as the phone has carried it out. No Lusor agent is involved.

Every direct-control endpoint lives under the phone:

```
https://api.lusor.ai/v1/workspaces/{workspaceId}/devices/{deviceId}/…
```

See [Phones overview](https://docs.lusor.ai/phones/overview.md) for how to find both ids.

> [!NOTE]
>
> On a Lusor Phone, direct control with an API key uses Lusor Phone minutes. See [Billing](#billing) below. Your own iPhone never uses them.

## Read the screen

`GET …/state` returns the screen's UI element tree:

```bash cURL theme={null}
curl https://api.lusor.ai/v1/workspaces/$WS/devices/$DEVICE/state \
  -H "X-Api-Key: $LUSOR_API_KEY"
```

```python Python theme={null}
import os, requests

BASE = f"https://api.lusor.ai/v1/workspaces/{os.environ['WS']}/devices/{os.environ['DEVICE']}"
H = {"X-Api-Key": os.environ["LUSOR_API_KEY"]}

state = requests.get(f"{BASE}/state", headers=H).json()
print(state["a11yTree"])
```

```javascript Node.js theme={null}
const BASE = `https://api.lusor.ai/v1/workspaces/${process.env.WS}/devices/${process.env.DEVICE}`;
const H = { "X-Api-Key": process.env.LUSOR_API_KEY };

const state = await (await fetch(`${BASE}/state`, { headers: H })).json();
console.log(state.a11yTree);
```

| Field | Meaning |
| - | - |
| `a11yTree` | The element tree as text. Each interactive element carries a 1-based index such as `[12]`, which you can tap directly. |
| `deviceContext` | `screenWidth`, `screenHeight`, `interactiveCount` and `elementCount`. |
| `phoneState` | Extra details the phone reports about its current state. |

For an image instead, `GET …/screenshot` takes a new screenshot and returns it as PNG bytes:

```bash theme={null}
curl https://api.lusor.ai/v1/workspaces/$WS/devices/$DEVICE/screenshot \
  -H "X-Api-Key: $LUSOR_API_KEY" -o screen.png
```

## Act

| Action | Endpoint | Body |
| - | - | - |
| Tap | `POST …/tap` | `{ "index": 12 }` or `{ "x": 540, "y": 1200 }` |
| Swipe | `POST …/swipe` | `{ "startX": 540, "startY": 1800, "endX": 540, "endY": 600, "durationMs": 300 }` |
| Type into the focused field | `POST …/type` | `{ "text": "hello", "clear": true }` |
| Press a key | `POST …/key` | `{ "keyCode": 66 }` |
| Clear the focused field | `POST …/clear` | none |
| Back, Home, Recent apps | `POST …/back`, `…/home`, `…/recent` | none |
| Launch an app | `POST …/apps/start` | `{ "package": "com.android.settings" }`, optional `activity` |
| Force-stop an app | `POST …/apps/stop` | `{ "package": "com.android.settings" }` |
| Take a screenshot | `POST …/screenshot` | none |

Details:

* **Tap.** Send `index` from the element tree, or `x` and `y` in pixels. If you send both, `index` wins. The response's `resolved` field has the point that was tapped.
* **Swipe.** `durationMs` is 1 to 60000.
* **Type.** `text` is 1 to 8192 characters. `clear: true` empties the field first.
* **Key.** `keyCode` is an Android key code. Common ones: 66 Enter, 67 Delete, 61 Tab, 111 Escape, 19 to 22 the arrow keys.
* **Apps.** `package` and `activity` must be dotted identifiers such as `com.android.settings`. Anything else is a `400`.
* **Screenshot.** `POST …/screenshot` returns the same result shape as the other actions, so it can be a step in a [chain](https://docs.lusor.ai/phones/chains-and-capture.md). Use `GET …/screenshot` for the image itself.

Each action returns `ok`, `exit_code` (`0` on success) and `status`. `ok: false` means the phone could not carry it out.

```bash cURL theme={null}
curl https://api.lusor.ai/v1/workspaces/$WS/devices/$DEVICE/tap \
  -H "X-Api-Key: $LUSOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "index": 12 }'
```

```python Python theme={null}
r = requests.post(f"{BASE}/tap", headers=H, json={"index": 12})
print(r.json()["ok"])
```

```javascript Node.js theme={null}
const r = await fetch(`${BASE}/tap`, {
  method: "POST",
  headers: { ...H, "Content-Type": "application/json" },
  body: JSON.stringify({ index: 12 }),
});
console.log((await r.json()).ok);
```

> [!WARNING]
>
> Actions are not idempotent. Retrying a tap that timed out may tap twice. Read the screen before you retry.

Add `capture` to any action to get the screen back in the same response. See [Chains and capture](https://docs.lusor.ai/phones/chains-and-capture.md).

## Human-like input

`tap`, `swipe` and `type` accept `"stealth": true`:

| Action | With `stealth` |
| - | - |
| `tap` | The press is held 45 to 110 ms instead of a single frame, and the point moves by up to 2 px. |
| `swipe` | The finger follows a curved, eased path. The duration is unchanged. |
| `type` | Each key waits 45 to 120 ms, with an occasional longer pause, up to 6 seconds in total. Limited to 750 characters; longer text returns `400 invalid_stealth_input`. |

If the phone cannot do human-like input, the request returns `412 unsupported_stealth_input`. It is never quietly sent as normal input.

## Your own iPhone

On your own iPhone, the UI element tree is not available through the API yet:

* `GET …/state` and `tap` with an `index` return `502`.
* A `capture` that asks for the element tree reports `capture.failed`.

Use `GET …/screenshot` and tap with `{ "x", "y" }` coordinates instead. `keyCode` values and app package names are Android ones.

## Billing

On a **Lusor Phone**, direct control with an API key uses Lusor Phone minutes, the same pool as agent runs:

* The first action opens a metered window. Every further action keeps it open. The window closes **60 seconds after the last action**, so a single action costs 60 seconds and a burst of actions costs its length plus 60 seconds.
* Reads count too. `GET …/state` and `GET …/screenshot` are metered like actions. A chain is metered before each step.
* Time that overlaps an agent run on the same phone is billed once, not twice.
* Your **own iPhone** never uses Lusor Phone minutes.

When a Lusor Phone cannot be used, the action is refused and **not sent** to the phone.

| Status | `error` | What to do |
| - | - | - |
| `402` | `credits_exhausted` | The workspace is out of credits for Lusor Phone minutes, or reached its plan's allowance with overages off. Buy credits, turn overages on, or upgrade. |
| `402` | `billing_required` | This Lusor Phone is not covered by an active paid plan. |
| `409` | `internet_route_inactive` | The phone's Internet connection is not active yet. Retry once it is. |
| `503` | `meter_unavailable` | Metering could not start. Nothing was sent. Retry shortly. |

For how minutes, credits and overages work across agent runs and direct control, see [Credits and minutes](https://docs.lusor.ai/billing/credits-and-minutes.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.