Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Start with the [Quick Start](getting-started/quickstart.md) to install the CLI a

### Automate with AI & code

* [Agent Devices](mcp/agent-devices.md) - give your AI agent a cloud iOS or Android device to drive, with a person on hand when it gets stuck (beta)
* [MCP Server](mcp/overview.md) - let Claude, Cursor and other MCP clients run your tests
* [CLI Reference](cli/overview.md) - every `dcd` command and flag
* [REST API](api/overview.md) - uploads, results and flows over HTTP
Expand Down
5 changes: 5 additions & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
* [Status](cli/dcd-status.md)
* [List](cli/dcd-list.md)
* [Login & Accounts](cli/dcd-login.md)
* [Devices](cli/dcd-device.md)
* [Live Sessions](cli/dcd-live.md)

## REST API
Expand All @@ -67,10 +68,14 @@
* [Uploads](api/uploads.md)
* [Results](api/results.md)
* [Flows](api/flows.md)
* [Device Sessions](api/device-sessions.md)
* [IP Addresses](api/ip-addresses.md)

## AI Agents & MCP

* [Agent Devices](mcp/agent-devices.md)
* [Agent Devices Tools](mcp/agent-devices-tools.md)
* [Agent Instructions](mcp/agent-devices-skill.md)
* [MCP Server](mcp/overview.md)
* [Tools Reference](mcp/tools.md)

Expand Down
101 changes: 101 additions & 0 deletions api/device-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Device sessions

The REST API behind [Agent Devices](../mcp/agent-devices.md). The MCP tools call these same endpoints. Authenticate with an [API key](../getting-started/api-keys.md) in `x-app-api-key`, as for the rest of the [REST API](overview.md#authentication). A key limited to **Device sessions only** can reach these endpoints, the hosted MCP server and the build upload endpoints, but nothing else.

A session is named by its `id` (for example `brave-teal-otter`) in every path. Every response includes a `session` block: its state, time and idle time left, cost so far, warnings and console link. [The tool reference](../mcp/agent-devices-tools.md#the-session-block) lists its fields.

## Sessions

| Method and path | What it does |
|---|---|
| `GET /device/models` | The devices you can start: models, OS versions and the defaults |
| `POST /device/sessions` | Start a session. Body: `platform` (`ios` or `android`), and optionally `model`, `os`, `googlePlay`, `binaryUploadId`, `deviceLocale` (such as `de_DE`), `maxDurationMin`, `clientLabel`, `record` (default `true`) |
| `GET /device/sessions` | Your organization's open sessions. `?all=true` adds those that ended in the last day |
| `GET /device/sessions/:id` | One session |
| `DELETE /device/sessions/:id` | Stop a session and release the device |

## The device

| Method and path | What it does |
|---|---|
| `POST /device/sessions/:id/install` | Install a build: `{ "binaryUploadId": "..." }`, or `{ "url": "https://..." }` for an `.apk` or a zipped simulator `.app` at a link (up to 500 MB). It installs in the background. A command sent meanwhile waits up to 45 s for it, then answers `LIVE_NOT_READY` |
| `POST /device/sessions/:id/launch` | Launch an app: `{ "appId"?: "...", "clearState"?: true }`. Without `appId`, the installed build |
| `POST /device/sessions/:id/actions` | One action: `tap` (`x`/`y` percent, `text` or `id`), `type` (`text`, and `sensitive: true` to keep it secret), `swipe` (`direction`: `up`, `down`, `left` or `right`, or `from`/`to` points in percent), `press_key` (`key`: `back`, `home`, `enter`, `backspace`, `tab`, `volume_up`, `volume_down`, `lock` or `power`) |
| `POST /device/sessions/:id/run` | Run Maestro: `{ "yaml": "..." }` for one flow, or `{ "flowUploadId": "...", "flow"?: "..." }` for a [flow bundle](#flow-bundles). `env` passes [secrets and variables](#secrets). Waits up to 110 s, or `waitSeconds` (0 to 300), then answers `running` with a `command_id`. A failed run's result includes `screen_text`, the app's visible text after the failure |
| `GET /device/sessions/:id/commands/:commandId` | The status and result of a `run` that answered `running`. `?waitSeconds=` (0 to 300) waits for it to finish instead of answering at once |
| `GET /device/sessions/:id/inspect` | The screen as compact JSON. The on-screen keyboard and the System UI are left out unless you add `?all=true` |
| `GET /device/sessions/:id/screenshot` | The screen as it is now, as a JPEG data URL. `fresh` is `false` when no new frame came within 3 s, so this is the latest there is |
| `GET /device/sessions/:id/logs` | The installed app's recent device log lines. `lines` (default 200, at most 2000), `level` (`warning` or `error`) and `grep` (any case) narrow them. The answer stops at 24 KB, with `truncated: true` when older lines were left out |

## Flow bundles

To run several flows, or a flow that runs others, upload them as a zip, then pass its id to `run` as `flowUploadId`. `flow` names the entry file inside the zip; without it, every flow runs. [`dcd device run <directory>`](../cli/dcd-device.md) does this for you.

| Method and path | What it does |
|---|---|
| `POST /device/bundles/upload-url` | `{ "bytes"?: 1234 }`. Returns the bundle's `id`, and signed URLs to `PUT` the zip to: `upload` and `mirror` (either may be `null`). Upload to each one given |
| `POST /device/bundles` | Register it: `{ "id", "sha", "bytes", "supabase": true, "backblaze": true }`, where `sha` is the SHA-256 of the zip in hex, and `supabase` and `backblaze` say which URLs you uploaded to. Returns `{ "bundle" }` |

A bundle can be up to 50 MB. Bundles are deleted a day after they're uploaded, once no command needs them.

## Asking a person

| Method and path | What it does |
|---|---|
| `POST /device/sessions/:id/request-human` | `{ "reason": "..." }`, up to 500 characters. The session waits for a person, with its idle timer paused, for up to 30 minutes. Your [webhook](../notifications/webhook-notifications.md#device-needs-human) gets a `device.needs_human` event |
| `GET /device/sessions/:id/human` | Whether it's still waiting, or was handed back, with the person's note |
| `POST /device/sessions/:id/hand-back` | `{ "note"?: "..." }`, up to 2000 characters. Hand the session back to the agent |

Taking control of a device is for people in the console. While someone has control, commands that change the device answer `LIVE_HUMAN_IN_CONTROL`, and those already queued wait until the hand-back.

## Artifacts

| Method and path | What it does |
|---|---|
| `GET /device/sessions/:id/timeline` | Every command and event, newest first: who sent it, its status, and the screenshot after it. Paged with `limit` (default 50, at most 200) and `before` (the last page's `next_before`) |
| `GET /device/sessions/:id/artifacts` | Short-lived links to the session's video, device log, and the screenshot and screen dump after each action. Paged the same way. The video and log arrive after the session ends |

## Secrets

`env` on `run` is an object of names to string values: up to 50 of them, each up to 4096 characters. Names are letters, digits and `_`, and don't start with a digit. Names starting with `DCD_` are reserved. Refer to them in the YAML as `${NAME}`. They're encrypted before they're stored, never shown in the timeline, and masked in the command's output and in `inspect`.

A `type` action with `sensitive: true` sends its text the same way.

## Rate limits

These endpoints have their own limits instead of the API's general one:

| What | Limit |
|---|---|
| Commands (install, launch, actions, run, asking a person) | 60 a minute, per session |
| Reads (status, inspect, screenshot, logs, timeline, artifacts) | 240 a minute, per session |
| Session lists, device models and bundle uploads | 120 a minute, per organization |

Over a limit, the API answers `LIVE_RATE_LIMITED` with how long to wait. Stopping an open session is never refused.

## Errors

Errors are JSON with a `message` and a stable `error` code:

| Code | Meaning |
|---|---|
| `LIVE_NOT_ENABLED` | Device sessions aren't enabled for your organization |
| `LIVE_CONCURRENCY_LIMIT` | Your organization has as many sessions open as it may |
| `LIVE_SPEND_CAP` | Your organization reached its monthly spend cap |
| `LIVE_NOT_FOUND` | No such session in your organization |
| `LIVE_NOT_READY` | The device is still starting, or installing an app: retry shortly |
| `LIVE_SESSION_ENDED` | The session has ended |
| `LIVE_INVALID_ACTION` | The request is malformed: the message says how |
| `LIVE_INVALID_APP` | The build isn't one this device can install |
| `LIVE_APP_DOWNLOAD_FAILED` | The build at the URL couldn't be downloaded |
| `LIVE_NO_APP` | No app is installed, and none was named |
| `LIVE_COMMAND_NOT_FOUND` | No such command in this session |
| `LIVE_LOGS_UNAVAILABLE` | The device logs couldn't be read |
| `LIVE_SESSION_REQUIRED` | MCP only: more than one session is open, so pass `session_id` |
| `LIVE_RATE_LIMITED` | Too many requests: slow down |
| `LIVE_NOT_WAITING_FOR_HUMAN` | Hand-back on a session that isn't waiting for a person |
| `LIVE_HUMAN_IN_CONTROL` | A person has taken control in the console: wait for them to hand back |
| `LIVE_CONSOLE_ONLY` | Only a person signed in to the console can do this (take control) |
| `LIVE_NOT_IN_CONTROL` | Only the person in control can do this (remove a held step) |
| `LIVE_SECRETS_UNAVAILABLE` | Secrets can't be accepted right now |
| `API_KEY_SCOPE` | The API key isn't allowed to reach this endpoint |
134 changes: 134 additions & 0 deletions cli/dcd-device.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# dcd device

Lease a cloud iOS simulator or Android emulator, drive it, and release it, from your terminal or a script. These are the same device sessions an AI agent uses through [Agent Devices](../mcp/agent-devices.md): you can watch any of them in the console, and they show on the same Devices page.

::: warning
**Device sessions are in a closed beta** and are billed at **$0.03/min**, excluding Sales Tax. Access is by request: contact support to have it turned on for your organization. `dcd device` works with a [`dcd login`](dcd-login.md) session or an [API key](../getting-started/api-keys.md).
:::

```bash
dcd device <subcommand> [flags]
```

A typical session:

```bash
dcd device start --platform ios --app ./build/MyApp.app --wait
dcd device launch
dcd device inspect
dcd device run flows/login.yaml
dcd device screenshot -o after-login.jpg
dcd device stop
```

## Rules for every subcommand

- **The session id is optional** when your organization has exactly one open session. With none, or several, the command stops and lists the sessions to choose from.
- **`--json`** prints exactly one JSON object on stdout, on success and on failure:

```json
{ "ok": true, "session": { "id": "brave-teal-otter", "state": "active", "...": "..." }, "data": { }, "error": null }
```

`session` is the same session block the [REST API](../api/device-sessions.md) returns: state, time and idle time left, cost so far, warnings, and the console link. `list`, `models` and `stop --all` leave it out, because they aren't about one session. On failure, `ok` is `false` and `error` is `{ "code", "message" }`, where `code` is the API's error code (such as `LIVE_SPEND_CAP`) or `null`.
- **Exit codes:**

| Code | Meaning |
|---|---|
| `0` | Done |
| `1` | Called wrongly: a bad flag or value, or no session to pick |
| `2` | It ran and failed: a flow or launch failed, or the API couldn't do it |
| `3` | A limit was hit: concurrent sessions, spend cap, or rate limit |
| `4` | The session has ended, or there is no such session |

- **Progress** goes to stderr, so stdout holds only the result. There are no prompts, and no spinners unless you are at a terminal.

Every subcommand also takes `--api-key` and `--api-url`.

## `dcd device start`

Start a session. Without `--wait` it returns at once, possibly with the session still queued; it starts on its own.

| Flag | Description |
|------|-------------|
| `--platform <ios\|android>` | The platform. Optional with `--model` or `--app`, which imply it |
| `--model <model>` | A device model, such as `iphone-16` or `pixel-10`. See `dcd device models` |
| `--os <version>` | The iOS major version or Android API level, such as `26` or `36` |
| `--google-play` | Android only: an image with Google Play services |
| `--app <path\|url>` | A build to install: a local `.apk`, `.app`, `.zip` or Expo `.tar.gz` (uploaded first), or an HTTPS link to an `.apk` or a zipped `.app` |
| `--app-binary-id <id>` | A build you already uploaded |
| `--locale <locale>` | The device locale, such as `de_DE` |
| `--name <label>` | Who is driving the session, shown in the console (default `dcd-cli`) |
| `--ttl <minutes>` | The longest the session may run (default: your organization's limit, at most 60) |
| `--no-record` | Don't record a video of the session |
| `--wait` | Wait until the device is ready for commands |

## `dcd device list`

Your organization's open sessions. `--all` adds those that ended in the last 24 hours.

## `dcd device status [id]`

A session's state, time left, idle timeout and cost so far.

## `dcd device stop [id]`

Stop a session and release the device. `--all` stops every open session in your organization.

## `dcd device models`

The devices a session can ask for, with their OS versions and the defaults. `--platform` shows one platform.

## `dcd device install [id] <path|url>`

Install a build: a local file (uploaded first), an HTTPS link, or `--app-binary-id <id>`. It installs in the background: a command sent meanwhile waits for the install, for up to 45 seconds. `--wait` waits until the device is ready again.

## `dcd device launch [id] [app-id]`

Launch an app: the installed one, or a bundle id or package name. `--clear-state` clears its data first. A failed launch exits `2`.

## `dcd device inspect [id]`

The screen as compact JSON: each element with its text, id, accessibility label or hint, any state (checked, selected, focused or disabled), and its bounds. Use these to target `tapOn` in a flow. The on-screen keyboard's keys and the System UI (the status and navigation bars) are left out; `--all` lists them too.

## `dcd device screenshot [id]`

Save a screenshot. `-o <file>` sets where (default `./<session>-<time>.jpg`). With `--json`, `data` gives the file's path.

## `dcd device run [id] <flow.yaml|directory>`

Run Maestro on the device and wait for the result. A failed flow exits `2`. Maestro's output is printed either way, and after a failure, so is the app's visible text.

| Flag | Description |
|------|-------------|
| `<flow.yaml>` | One flow file, run whole |
| `<directory>` | A directory of flows, uploaded as a bundle. Every flow in it runs, unless you pass `--entry` |
| `--entry <path>` | With a directory: the one flow to run, as a path inside it, such as `flows/login.yaml` |
| `--yaml <flow>` | The flow inline, instead of a file |
| `--env KEY=VALUE` | A variable for the flow, used as `${KEY}`. Repeat it for more. Values are encrypted before they're stored, and masked in the output |

For a directory, only the YAML files and the files the flows use, such as scripts and media, are uploaded, up to 50 MB.

## `dcd device logs [id]`

The installed app's recent device log lines. `--lines <n>` sets how many (default 200, at most 2000). `--follow` (`-f`) keeps printing new lines until the session ends (not with `--json`).

## `dcd device request-human <reason> [id]`

Ask a person for help, such as entering a 2FA code or solving a CAPTCHA. The session waits for them, with its idle timer paused, and they see your reason on the session's page in the console. Your organization's [webhook](../notifications/webhook-notifications.md#device-needs-human) gets a `device.needs_human` event. The reason can be up to 500 characters. `--wait` waits until they hand the session back.

## `dcd device human [id]`

Whether a session is waiting for a person, or a person has control, and their note once they hand it back. `--wait` waits for the hand-back. Ctrl-C stops waiting and leaves the session as it is.

## `dcd device open [id]`

Open the session's page in the console, where you can watch the device and take control. In a script, it prints the link instead.

## `dcd device artifacts [id]`

Download what a session left behind into `./<session>-artifacts` (or `-o <dir>`): `recording.mp4`, `device.log`, and the screenshot and screen dump after each action (`actions/001-screenshot.jpg`, `actions/001-screen.json`, …), numbered in order. `--links` lists the download links instead. The video and log are saved when the session ends.

## Replacing `dcd live`

`dcd device` replaces [`dcd live`](dcd-live.md), which still works but is deprecated. The main differences: the session id is a positional argument instead of `--session`, `run` takes a directory, and every command has `--json` and meaningful exit codes.
4 changes: 4 additions & 0 deletions cli/dcd-live.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# dcd live

::: info
**`dcd live` is deprecated.** Use [`dcd device`](dcd-device.md), which drives the same sessions with a session id argument, directory runs, `--json` on every command and meaningful exit codes. `dcd live` keeps working for now.
:::

Start an interactive cloud device session and drive it from your terminal — install a build, run individual Maestro commands or a whole flow, capture screenshots, and dump the view hierarchy. This is ideal for authoring and debugging flows against a real cloud device.

::: warning
Expand Down
6 changes: 4 additions & 2 deletions cli/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ When both are present, precedence is: `--api-key` flag → `DEVICE_CLOUD_API_KEY
| [`dcd logout`](dcd-login.md#dcd-logout) | Clear the stored session |
| [`dcd whoami`](dcd-login.md#dcd-whoami) | Show the logged-in user and active organisation |
| [`dcd switch-org`](dcd-login.md#dcd-switch-org) | Switch the active organisation |
| [`dcd live`](dcd-live.md) | Start and interact with a live device session (beta) |
| [`dcd device`](dcd-device.md) | Lease a cloud device, drive it and release it (beta) |
| [`dcd mcp`](../mcp/agent-devices.md#headless-through-the-cli) | Run the DeviceCloud MCP server on stdio |
| [`dcd live`](dcd-live.md) | Deprecated: use `dcd device` |
| `dcd upgrade` | Upgrade the standalone binary in place |

## Environment Variables
Expand Down Expand Up @@ -128,5 +130,5 @@ To opt out, set `DCD_TELEMETRY_DISABLED=1`.
```bash
dcd --help
dcd cloud --help
dcd live --help
dcd device --help
```
Loading
Loading