# Devices API

> Create, list, inspect and stop cloud Android devices; take screenshots, send taps and text, install apps, snapshot, and mint stream tokens.

Source: https://akaphone-docs.pages.dev/api/devices/

A device is a cloud Android phone. Read [Devices](https://akaphone-docs.pages.dev/concepts/devices/) for the lifecycle and billing rules. All examples assume your key is in `AKAPHONE_API_KEY`.

## The device object

```json
{
  "id": "dev_s6zr7mr4f2f1",
  "name": "checkout-test",
  "status": "running",
  "profile": "pixel_8",
  "android": "14",
  "region": "eu-central",
  "node_id": "nod_01m5vqsywg84jv64rmmqf9",
  "snapshot_id": null,
  "created_at": "2026-10-26T09:14:02.000Z",
  "ready_at": "2026-10-26T09:14:09.000Z",
  "terminated_at": null,
  "last_activity_at": "2026-10-26T09:16:40.000Z",
  "billed_minutes": 3,
  "error": null
}
```

| Field | Type | Description |
|---|---|---|
| `id` | string | `dev_` and 12 characters. |
| `name` | string or null | Your label. |
| `status` | string | `queued`, `creating`, `running`, `stopping`, `terminated`, `error` or `lost`. |
| `profile` | string | Screen profile, default `pixel_8`. |
| `android` | string | Android version, `"14"`. |
| `region` | string | Where it runs, `eu-central`. |
| `node_id` | string or null | The host it was placed on; `null` while queued. |
| `snapshot_id` | string or null | The snapshot it was restored from. |
| `created_at` | string | When you created it. |
| `ready_at` | string or null | When it became `running`. Billing starts here. |
| `terminated_at` | string or null | When it ended. |
| `last_activity_at` | string | Last input, task, install or viewer activity. Used for the 30-minute idle stop. |
| `billed_minutes` | integer | Credits charged for this device so far. |
| `error` | string or null | Why it failed, e.g. `no_capacity`. |

## List profiles

`GET /v1/profiles`

The device profiles you can pass as `profile`. No authentication needed; cacheable for 5 minutes.

```bash title="curl"
curl https://akaphone.abhgnx-417.workers.dev/v1/profiles
```
```typescript title="TypeScript"
const profiles = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/profiles").then((r) => r.json());
```
```python title="Python"
profiles = requests.get("https://akaphone.abhgnx-417.workers.dev/v1/profiles").json()
```

**Response** `200 OK`

```json
[
  { "id": "pixel_8", "name": "Pixel 8", "width": 720, "height": 1560, "dpi": 320, "android": "14" },
  { "id": "pixel_tablet", "name": "Pixel Tablet", "width": 1280, "height": 800, "dpi": 240, "android": "14" },
  { "id": "small_phone", "name": "Small phone", "width": 540, "height": 960, "dpi": 240, "android": "14" }
]
```

`width` and `height` are the device pixels that [input](#send-input) coordinates use. `pixel_8` is the default.

## Create a device

`POST /v1/devices`

Queues a new device and returns it. With `wait`, the request is held until the device is out of `queued` and `creating`, for at most 90 seconds.

| Body field | Type | Default | Description |
|---|---|---|---|
| `name` | string | `null` | Up to 64 characters. |
| `profile` | string | `"pixel_8"` | `pixel_8`, `pixel_tablet` or `small_phone`; see [list profiles](#list-profiles). |
| `android` | string | `"14"` | One or two digits. |
| `snapshot_id` | string | `null` | Boot from this `ready` snapshot. Same as [restore](https://akaphone-docs.pages.dev/api/snapshots/#restore-a-snapshot). |
| `region` | string | first region | Must be an available region; today only `eu-central`. |
| `wait` | boolean | `false` | Hold the request until the device is running (max 90 s). |

The query parameters `?wait=true&timeout=<seconds>` do the same as `wait`; `timeout` is capped at 90.

```bash title="curl"
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/devices \
  -H "Authorization: Bearer $AKAPHONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "checkout-test", "wait": true}'
```
```typescript title="TypeScript"
const res = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "checkout-test", wait: true }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const device = await res.json();
```
```python title="Python"
import os, requests

r = requests.post(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
    json={"name": "checkout-test", "wait": True},
)
r.raise_for_status()
device = r.json()
```

**Response** `201 Created` with the [device](#the-device-object). Without `wait` its status is usually `queued`. With `wait` it is normally `running`; if boot failed it is `error`. If the 90 seconds ran out while the device was still `queued` or `creating`, the response carries the header **`X-Wait-Timed-Out: 1`**: poll [get a device](#get-a-device) until it is `running`.

```http
HTTP/1.1 201 Created
Content-Type: application/json
X-Wait-Timed-Out: 1

{"id": "dev_s6zr7mr4f2f1", "status": "creating", "…": "…"}
```

| Error | When |
|---|---|
| `402 insufficient_credits` | Balance below 1 credit. |
| `409 concurrency_limit` | You already have your plan's maximum of queued, creating and running devices. |
| `404 not_found` | `snapshot_id` does not exist. |
| `409 conflict` | The snapshot is `lost`, `pending` or `error`. |
| `400 invalid_request` | A field is invalid, the region is unknown, or the profile is not one of `pixel_8`, `pixel_tablet`, `small_phone`. |
| `429 rate_limited` | More than 10 device creations in a minute. |

## List devices

`GET /v1/devices`

Returns a JSON **array** of devices, newest first. By default only active devices (`queued`, `creating`, `running`, `stopping`).

| Query | Description |
|---|---|
| `status=all` | Include finished devices (the 200 most recent). `all=true` is accepted too. |

```bash title="curl"
curl "https://akaphone.abhgnx-417.workers.dev/v1/devices?status=all" \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
```
```typescript title="TypeScript"
const devices = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices?status=all", {
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
}).then((r) => r.json());
```
```python title="Python"
devices = requests.get(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices",
    params={"status": "all"},
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
).json()
```

**Response** `200 OK`

```json
[
  { "id": "dev_s6zr7mr4f2f1", "name": "checkout-test", "status": "running", "profile": "pixel_8", "android": "14", "region": "eu-central", "node_id": "nod_01m5vqsywg84jv64rmmqf9", "snapshot_id": null, "created_at": "2026-10-26T09:14:02.000Z", "ready_at": "2026-10-26T09:14:09.000Z", "terminated_at": null, "last_activity_at": "2026-10-26T09:16:40.000Z", "billed_minutes": 3, "error": null },
  { "id": "dev_ynxe87v87zx0", "name": null, "status": "terminated", "profile": "pixel_8", "android": "14", "region": "eu-central", "node_id": "nod_01m5vqsywg84jv64rmmqf9", "snapshot_id": null, "created_at": "2026-10-25T17:02:11.000Z", "ready_at": "2026-10-25T17:02:19.000Z", "terminated_at": "2026-10-25T17:21:40.000Z", "last_activity_at": "2026-10-25T17:21:40.000Z", "billed_minutes": 20, "error": null }
]
```

## Get a device

`GET /v1/devices/:id`

```bash title="curl"
curl https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1 \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
```
```typescript title="TypeScript"
const device = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1", {
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
}).then((r) => r.json());
```
```python title="Python"
device = requests.get(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
).json()
```

**Response** `200 OK` with the [device](#the-device-object). `404 not_found` if it does not exist or is not yours.

## Stop a device

`DELETE /v1/devices/:id`

Stops the device and its billing. Idempotent: stopping a device that has already ended returns it unchanged.

```bash title="curl"
curl -X DELETE https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1 \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
```
```typescript title="TypeScript"
await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
});
```
```python title="Python"
requests.delete(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
)
```

**Response** `202 Accepted` with the device: `stopping` if it was creating or running (it becomes `terminated` shortly after), or `terminated` if it was still queued.

```json
{ "id": "dev_s6zr7mr4f2f1", "name": "checkout-test", "status": "stopping", "profile": "pixel_8", "android": "14", "region": "eu-central", "node_id": "nod_01m5vqsywg84jv64rmmqf9", "snapshot_id": null, "created_at": "2026-10-26T09:14:02.000Z", "ready_at": "2026-10-26T09:14:09.000Z", "terminated_at": null, "last_activity_at": "2026-10-26T09:31:15.000Z", "billed_minutes": 18, "error": null }
```

## Take a screenshot

`GET /v1/devices/:id/screenshot`

Returns the current screen as `image/jpeg`. If someone is watching the live stream you get the latest frame; otherwise the host captures one.

```bash title="curl"
curl https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/screenshot \
  -H "Authorization: Bearer $AKAPHONE_API_KEY" \
  -o screen.jpg
```
```typescript title="TypeScript"
import { writeFile } from "node:fs/promises";

const res = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/screenshot", {
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
});
await writeFile("screen.jpg", Buffer.from(await res.arrayBuffer()));
```
```python title="Python"
r = requests.get(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/screenshot",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
)
open("screen.jpg", "wb").write(r.content)
```

**Response** `200 OK`, `Content-Type: image/jpeg`. Frames are downscaled by the host (540 px wide by default).

| Error | When |
|---|---|
| `409 conflict` | The device is not `running`. |
| `504 internal` | No frame arrived within 8 seconds. Retry. |

## Send input

`POST /v1/devices/:id/input`

Sends one tap, swipe, text or key event. Coordinates are in device pixels (720×1560 for `pixel_8`).

| `type` | Fields |
|---|---|
| `tap` | `x`, `y`: integers 0–10000 |
| `swipe` | `x1`, `y1`, `x2`, `y2`: integers 0–10000; `ms`: 1–10000, default 300 |
| `text` | `text`: 1–500 characters |
| `key` | `key`: `HOME`, `BACK`, `RECENTS`, `ENTER`, `DEL` or `TAB` |

```bash title="curl"
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/input \
  -H "Authorization: Bearer $AKAPHONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "tap", "x": 360, "y": 780}'
```
```typescript title="TypeScript"
const input = (body: object) =>
  fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/input", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });

await input({ type: "tap", x: 360, y: 780 });
await input({ type: "text", text: "hello@example.com" });
await input({ type: "swipe", x1: 360, y1: 1200, x2: 360, y2: 400, ms: 250 });
await input({ type: "key", key: "ENTER" });
```
```python title="Python"
def send_input(body: dict):
    return requests.post(
        "https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/input",
        headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
        json=body,
    )

send_input({"type": "tap", "x": 360, "y": 780})
send_input({"type": "text", "text": "hello@example.com"})
send_input({"type": "key", "key": "ENTER"})
```

**Response** `202 Accepted`

```json
{ "ok": true }
```

The event is sent to the device; there is no confirmation that it had an effect. Take a screenshot to check. For many events in a row, the [stream socket](https://akaphone-docs.pages.dev/api/stream/#sending-input) is faster than one HTTP request each.

| Error | When |
|---|---|
| `400 invalid_request` | Unknown `type`, missing field, or value out of range. |
| `409 conflict` | The device is not `running`, or its host is not connected. |

## Install an app

`POST /v1/devices/:id/apps`

Installs a completed [upload](https://akaphone-docs.pages.dev/api/uploads/) on the device.

| Body field | Type | Description |
|---|---|---|
| `upload_id` | string | An upload in status `uploaded`. |

```bash title="curl"
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/apps \
  -H "Authorization: Bearer $AKAPHONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"upload_id": "upl_01m5vqsywgw57haqkjnexz"}'
```
```typescript title="TypeScript"
const { install_id } = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/apps", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ upload_id: "upl_01m5vqsywgw57haqkjnexz" }),
}).then((r) => r.json());
```
```python title="Python"
install_id = requests.post(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/apps",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
    json={"upload_id": "upl_01m5vqsywgw57haqkjnexz"},
).json()["install_id"]
```

**Response** `202 Accepted`

```json
{ "install_id": "ins_01m5vqsywg99189k5xj7f5" }
```

The host downloads and installs the APK in the background (`adb install -r -g`). Poll [get an install](#get-an-install) for the outcome, or watch for the [`app.installed`](https://akaphone-docs.pages.dev/api/stream/#events) event on the live stream. See [Install an APK](https://akaphone-docs.pages.dev/guides/install-apk/).

| Error | When |
|---|---|
| `404 not_found` | Unknown upload. |
| `409 conflict` | The upload is not complete, the device is not `running`, or its host is not connected. |

## Get an install

`GET /v1/devices/:id/apps/:install_id`

The status of an install started with [install an app](#install-an-app). Poll it every few seconds until `status` is no longer `pending`.

```bash title="curl"
curl https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/apps/ins_01m5vqsywg99189k5xj7f5 \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
```
```typescript title="TypeScript"
const install = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/apps/ins_01m5vqsywg99189k5xj7f5", {
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
}).then((r) => r.json());
```
```python title="Python"
install = requests.get(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/apps/ins_01m5vqsywg99189k5xj7f5",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
).json()
```

**Response** `200 OK`

```json
{
  "install_id": "ins_01m5vqsywg99189k5xj7f5",
  "status": "installed",
  "package": "com.example.shop",
  "created_at": "2026-10-26T09:20:00.000Z",
  "finished_at": "2026-10-26T09:20:06.000Z"
}
```

| Field | Description |
|---|---|
| `status` | `pending`, `installed` or `failed`. |
| `package` | The installed package name, when it could be determined. Omitted otherwise. |
| `error` | Why it failed, e.g. `INSTALL_FAILED_NO_MATCHING_ABIS` or `timeout`. Omitted on success. |
| `finished_at` | When it ended. Omitted while `pending`. |

An install still `pending` after 15 minutes becomes `failed` with `error: "timeout"`. Install records are kept for 30 days. `404 not_found` for an unknown device or install id.

## Snapshot a device

`POST /v1/devices/:id/snapshot`

Saves the device's disk as a [snapshot](https://akaphone-docs.pages.dev/concepts/snapshots/).

| Body field | Type | Description |
|---|---|---|
| `name` | string | Optional, up to 64 characters. |

```bash title="curl"
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/snapshot \
  -H "Authorization: Bearer $AKAPHONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "signed-in-v2.3"}'
```
```typescript title="TypeScript"
const snapshot = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/snapshot", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ name: "signed-in-v2.3" }),
}).then((r) => r.json());
```
```python title="Python"
snapshot = requests.post(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/snapshot",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
    json={"name": "signed-in-v2.3"},
).json()
```

**Response** `202 Accepted`

```json
{
  "id": "snp_01m5vqsywgez2ntdv5fq42",
  "device_id": "dev_s6zr7mr4f2f1",
  "node_id": "nod_01m5vqsywg84jv64rmmqf9",
  "name": "signed-in-v2.3",
  "size_bytes": null,
  "status": "pending",
  "created_at": "2026-10-26T09:20:44.000Z"
}
```

| Error | When |
|---|---|
| `409 conflict` | The device is not `running`; your snapshot quota is reached; the host is low on disk or not connected. |
| `429 rate_limited` | More than 5 snapshots in an hour. |

## Create a stream token

`POST /v1/devices/:id/stream-token`

Returns a single-use token, valid for 5 minutes, that lets a browser open this device's [live stream](https://akaphone-docs.pages.dev/api/stream/) without your API key. Call it from your server and hand the token to the page.

```bash title="curl"
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/stream-token \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
```
```typescript title="TypeScript"
const { token } = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/stream-token", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
}).then((r) => r.json());
```
```python title="Python"
token = requests.post(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/stream-token",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
).json()["token"]
```

**Response** `200 OK`

```json
{ "token": "pfs_Jq3w0kV9mXr2YbT6nLc8Hd1sPfAe4uZo" }
```

The token is bound to this device. It works for one connection; reconnecting needs a new token.

## Related

- [Run a task on a device](https://akaphone-docs.pages.dev/api/tasks/#run-a-task): `POST /v1/devices/:id/tasks`
- [Live stream](https://akaphone-docs.pages.dev/api/stream/): `GET /v1/devices/:id/stream` (WebSocket)
- [Restore a snapshot](https://akaphone-docs.pages.dev/api/snapshots/#restore-a-snapshot): `POST /v1/snapshots/:id/restore`
