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

# Mobile devices

> Cloud Android phones you look at, tap, type into, and destroy. The third member of the computer suite.

<Info>See [Computers](/docs/api/computers) for the Linux sandbox and [Browser sessions](/docs/api/browser-sessions) for the managed browser. A mobile device is a sibling of both: its own resource, with its own lifecycle. It needs no computer.</Info>

A **mobile device** is a cloud phone: a screen, touch input and an app lifecycle. It is ready in about a minute, driven through a small set of actions, watched live from the dashboard, and ends when you delete it or when its own timers fire. There is **no pause**: idle cost is controlled by `idle_timeout_minutes` and `max_duration_minutes`, both set at create; the platform checks them every minute.

## The mobile device object

<ResponseField name="id" type="string">Unique id (e.g. `mob_...`).</ResponseField>
<ResponseField name="object" type="string">Always `mobile_device`.</ResponseField>
<ResponseField name="name" type="string">Human-readable name. The one field you can change after create.</ResponseField>
<ResponseField name="platform" type="string">`android`.</ResponseField>
<ResponseField name="os_version" type="string">`13`, `14` or `15`.</ResponseField>
<ResponseField name="model" type="string">`phone`. `tablet` and `watch` are reserved and refused today.</ResponseField>
<ResponseField name="jurisdiction" type="string">`us`, `eu` or `as` — where the device is allowed to run. It is the one placement control; a region is never a parameter.</ResponseField>
<ResponseField name="status" type="string">`creating`, `ready`, `terminated` or `failed`. See [Truth](#truth-who-says-what-the-status-is).</ResponseField>
<ResponseField name="idle_timeout_minutes" type="integer">Minutes without an action after which the platform ends the device (1–180, default 10). Checked once a minute.</ResponseField>
<ResponseField name="max_duration_minutes" type="integer">Hard ceiling on the device's life (1–1440, default 120).</ResponseField>
<ResponseField name="apps" type="string[]">Package names (`com.example.app`) of apps in the platform's curated app catalogue, installed when the device starts. There is no upload route. There is no install route for a running device.</ResponseField>
<ResponseField name="termination_reason" type="string | null">`null` while alive; then `requested`, `idle_timeout`, `max_duration`, `unfunded` (stopped by the platform because the balance or plan no longer covered it) or `unknown`.</ResponseField>
<ResponseField name="error" type="string | null">One sentence when provisioning failed, else `null`.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>
<ResponseField name="terminated_at" type="string | null">When it ended, or `null` while alive.</ResponseField>
<ResponseField name="session_id" type="string | null">The agent session whose [`mobile` tool](/docs/capabilities/tools#the-mobile-tool) opened it, the session it was created for (`session_id` at create), or `null`.</ResponseField>

## Create a mobile device

`POST /v1/mobile_devices` → `202 Accepted` — scope `computers:write`. The row is recorded before the provider is asked, so a create whose answer is lost is still found and ended, never a device nobody can find; a provision that failed leaves the list. The request then waits up to \~45 seconds for the device to become `ready` and answers with whatever is true at that moment. A phone usually takes about a minute, so expect `creating` and poll `GET /v1/mobile_devices/{id}`. There is no limit on how many devices an organization runs, only on what it can pay for: a create is admitted when the prepaid balance covers **10 minutes of runway (`$0.39`) for every live device and the new one**, plus what the live ones have run so far — one device needs `$0.39`, a fifth `$1.95` plus the minutes the four running have accrued. Short of that, the create is `402 insufficient_credits` before anything is provisioned, and the message names the amount needed. When every phone slot on the platform is in use, the create is `429 rate_limited`; try again shortly. A device bills `$0.039` per minute from create to end, booked when it ends ([Pricing](/docs/platform/pricing#priced-tools)).

<ParamField body="name" type="string">Defaults to `device`.</ParamField>
<ParamField body="platform" type="string">`android`, the default and today the only value; `ios` is `400 validation_failed`.</ParamField>
<ParamField body="os_version" type="string">`13` | `14` | `15`, defaults to `15`.</ParamField>
<ParamField body="model" type="string">`phone`, the default and today the only value; `tablet` and `watch` are `400 validation_failed`.</ParamField>
<ParamField body="jurisdiction" type="string">`us` | `eu` | `as`. Defaults to `us`.</ParamField>
<ParamField body="idle_timeout_minutes" type="integer">1–180. Defaults to `10`.</ParamField>
<ParamField body="max_duration_minutes" type="integer">1–1440. Defaults to `120`.</ParamField>
<ParamField body="apps" type="string[]">Up to 10 package names (`com.example.app`) of apps in the platform's curated app catalogue, installed when the device starts.</ParamField>
<ParamField body="session_id" type="string">An agent session of yours. The device is filed under it, and that session's [`mobile` tool](/docs/capabilities/tools#the-mobile-tool) uses it instead of opening its own phone. When the session ends, the device ends with it. A session that has already ended is refused with `409 session_terminal`.</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/mobile_devices \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -H "idempotency-key: $(uuidgen)" \
    -d '{ "name": "pixel", "os_version": "15", "idle_timeout_minutes": 15 }'
  ```

  ```typescript TypeScript theme={"system"}
  const device = await vetta.mobileDevices.create({ name: "pixel", os_version: "15", idle_timeout_minutes: 15 });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={"system"}
  {
    "id": "mob_01H8YZ...",
    "object": "mobile_device",
    "name": "pixel",
    "platform": "android",
    "os_version": "15",
    "model": "phone",
    "jurisdiction": "us",
    "status": "ready",
    "idle_timeout_minutes": 15,
    "max_duration_minutes": 120,
    "apps": [],
    "termination_reason": null,
    "error": null,
    "created_at": "2026-09-22T10:00:00Z",
    "terminated_at": null,
    "session_id": null
  }
  ```
</ResponseExample>

## Retrieve & list

```bash theme={"system"}
GET /v1/mobile_devices/{id}   # retrieve one -> 200 (reconciled with the provider)
GET /v1/mobile_devices        # list (cursor-paginated), terminated and failed devices excluded -> 200
GET /v1/mobile_devices?session_id=ses_...   # only the phone that agent session opened
```

Both take any authenticated key. See [Pagination](/docs/api/pagination) for list parameters.

### Truth: who says what the status is

The platform ends a device on its idle and maximum timers, checking once a minute; the provider can also end one on its own (an error, a stopped account) without telling us. `status` on a row is what we last wrote; **`GET /v1/mobile_devices/{id}` asks the provider and rewrites the row** when they disagree, filling `terminated_at` and `termination_reason`. The **list reads rows only** — a provider call per row is not a read — so a list can lag until a detail is opened. Neither read wakes or bills anything: a device bills while it is alive whether or not you look at it.

## Rename

`PATCH /v1/mobile_devices/{id}` → `200 OK` — scope `computers:write`. The body is strict: any field other than `name` is `400` `validation_failed` naming it. Recorded in the [audit log](/docs/api/audit-logs) as `mobile_device.updated`.

<ParamField body="name" type="string" required>1–200 characters.</ParamField>

## Act on the device

`POST /v1/mobile_devices/{id}/act` → `200 OK` — scope `computers:write`. One action per call; the device must be `ready`, otherwise `409` `computer_unavailable`. Not written to the audit log: an action is work, as `exec` on a computer is.

| `type` | Fields | What it does |
| - | - | - |
| `screenshot` | — | The screen in `image`, a JPEG at half the phone's resolution. Its pixels map 1:1 to `tap` coordinates. |
| `element_tree` | — | `output` is `{ screen: { width, height }, elements: [{ text, resource_id, content_desc, class, clickable, bounds }] }`: the named elements on screen, `bounds` as `[left, top, right, bottom]` in screenshot pixels. |
| `tap` | `x`, `y` **or** `selector` | Tap a point, or the element a selector names. `selector` is `{ text?, resource_id?, content_desc? }`, at least one; the first matching element is tapped, an exact text match before a partial one. |
| `type` | `text` | Type into the focused field. |
| `press_key` | `key` | `HOME`, `BACK`, `RECENTS`, `NOTIFICATIONS`, `ENTER`, `DEL`, `TAB`, `ESCAPE`, `SPACE`, arrows (`DPAD_*`), volume, `POWER`, or a numeric key code. Case-insensitive; a `KEYCODE_` prefix is accepted. |
| `scroll` | `direction`, `amount?` | `up`, `down`, `left` or `right` — `down` shows what is below. `amount` is in screenshot pixels, 40% of the screen by default. |
| `open_url` | `url` | Open a URL or deep link. |

The selector words match the screen's elements:

| ours | matches |
| - | - |
| `text` | the element's visible text |
| `resource_id` | its `resource-id` |
| `content_desc` | its accessibility label (`content-desc`) |

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/mobile_devices/mob_01H8YZ.../act \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -d '{ "type": "tap", "x": 360, "y": 1200 }'
  ```

  ```typescript TypeScript theme={"system"}
  const shot = await vetta.mobileDevices.act("mob_01H8YZ...", { type: "screenshot" });
  await vetta.mobileDevices.act("mob_01H8YZ...", { type: "tap", selector: { text: "Sign in" } });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={"system"}
  {
    "object": "mobile_action_result",
    "type": "screenshot",
    "output": null,
    "image": { "content_type": "image/jpeg", "data": "/9j/4AAQSkZJRg..." }
  }
  ```
</ResponseExample>

`image` is the screen, filled only by `screenshot`. Every other action answers with `image: null` and a one-line account of what ran in `output`, so take a screenshot to see its result. A tap by `selector` taps the centre of the first element that matches, an exact `text` beating a partial one; a selector nothing matches is `409 computer_unavailable`.

## Open a live view

`POST /v1/mobile_devices/{id}/live_view` → `200 OK` — scope `computers:write`. Takes no body. Opening it counts as activity for the idle timer, as an action does; taps through the stream do not reach the API, so a client that keeps a phone open only through the stream should take a screenshot now and then.

<Warning>
  `stream` is a **bearer credential**: a `wss://` address and a token that let anyone holding them see and drive the device with no further authentication. It is minted on demand, never stored, never written to the audit trail, never returned inside any other object, and withheld from the [MCP catalogue](/docs/api/mcp). `expires_at` is `null`: it lives as long as the device does.
</Warning>

`url` is the device's page on the dashboard, where the stream plays for a signed-in member of your organization. It is a link, not a credential. `stream` is what the dashboard's realtime player takes: it is returned only to a signed-in member, and is `null` for an API key (open `url` instead) or when no stream is offered.

`409 computer_unavailable` while the device is still starting, `404 not_found` once it has ended. Audited as `mobile_device.live_view_opened` — who and when, never the credential.

<ResponseExample>
  ```json Response theme={"system"}
  {
    "object": "mobile_live_view",
    "url": "https://app.usenaive.ai/mobile/mob_01H8YZ...",
    "expires_at": null,
    "stream": { "url": "wss://...", "token": "..." }
  }
  ```
</ResponseExample>

## Delete a mobile device

`DELETE /v1/mobile_devices/{id}` → `200 OK` — scope `computers:write`. Ends the device at the provider and marks the row `terminated` with `termination_reason: "requested"`. Idempotent. Audited as `mobile_device.deleted`.

```json Response theme={"system"}
{ "id": "mob_01H8YZ...", "object": "mobile_device", "deleted": true }
```

## Errors

| Code | Status | When |
| - | - | - |
| `validation_failed` | 400 | A bad `os_version`, a `platform` other than `android`, a `model` other than `phone`, an app that is not a package name, a tap with no target, an unknown key. |
| `insufficient_credits` | 402 | Creating a device when the balance does not cover 10 minutes for every live device and the new one, plus what the live ones have run (an agent's phone counts too). |
| `not_found` | 404 | Not your device, or a live view on one that has ended. |
| `session_terminal` | 409 | Creating a device with the `session_id` of a session that has ended. |
| `computer_unavailable` | 409 | Acting on a device that is not `ready`; a selector that matches nothing; the provider did not answer a create in time. Its row stays `creating` for about 5 minutes while the platform checks, then leaves the list, and any phone that appeared is ended unbilled; it counts toward the balance check until then. |
| `rate_limited` | 429 | No device capacity is available on the platform right now; try again shortly. |
| `feature_not_configured` | 501 | This deployment cannot provision mobile devices; the create and the list answer it before anything is written. |


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