# Phones

Source: https://phonebox.dev/docs/api-reference/phones

> Create, list, get, update, delete, start, park and keep phones awake, and the phone object these endpoints return.



These endpoints manage phones and their sessions. [Lifecycle and timers](/docs/using-phones/lifecycle) explains the model behind them, and the [conventions](/docs/api-reference) and [common errors](/docs/api-reference#common-errors) apply to every one of them.

## The phone object [#the-phone-object]

```json title="Response: phone"
{
  "id": "ph_7kx2m6q4v3ta",
  "object": "phone",
  "name": "checkout-test",
  "status": "ready",
  "country": null,
  "metadata": { "customer": "acme" },
  "created_at": "2026-09-29T10:00:00.412Z",
  "last_active_at": "2026-09-29T10:03:18.905Z",
  "session": {
    "id": "ses_q2w6e4r5t3y7",
    "started_at": "2026-09-29T10:00:00.412Z",
    "ready_at": "2026-09-29T10:01:04.771Z",
    "idle_timeout": 300,
    "max_duration": 1800,
    "parks_at": "2026-09-29T10:08:18.905Z",
    "park_reason": "idle",
    "reserved_usd": "1.800000"
  },
  "failure": null
}
```

| Field            | Type              | Meaning                                                                                                                                                                                |
| ---------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`             | string            | The phone's ID: `ph_` followed by 12 characters from `a` to `z` and `2` to `7`.                                                                                                        |
| `object`         | string            | Always `phone`.                                                                                                                                                                        |
| `name`           | string            | Up to 80 characters. It is the phone's ID when you gave no name.                                                                                                                       |
| `status`         | string            | `creating`, `starting`, `ready`, `parking`, `parked`, `unavailable`, `failed` or `deleted`. [Concepts](/docs/getting-started/concepts#statuses) describes each one.                    |
| `country`        | string or null    | The two-letter country code asked for at create, or `null`.                                                                                                                            |
| `metadata`       | object            | Your string pairs, or `{}`.                                                                                                                                                            |
| `created_at`     | timestamp         | When the phone was created.                                                                                                                                                            |
| `last_active_at` | timestamp or null | When the phone was last active: when it became ready, or when a request last addressed it while it was ready, to within about 15 seconds. It is `null` until the phone is first ready. |
| `session`        | object or null    | The open session, or `null` when the phone isn't running.                                                                                                                              |
| `failure`        | object or null    | The latest failure, as `code`, `message` and `at`, or `null`. It is cleared when the phone starts again or becomes ready.                                                              |

The `session` object has these fields:

| Field          | Type              | Meaning                                                                                                                                 |
| -------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | string            | The session's ID: `ses_` followed by 12 characters.                                                                                     |
| `started_at`   | timestamp         | When the session was admitted. Billing starts here.                                                                                     |
| `ready_at`     | timestamp or null | When the phone became ready in this session, or `null` until it does.                                                                   |
| `idle_timeout` | integer           | Seconds after `last_active_at` at which the phone parks itself.                                                                         |
| `max_duration` | integer           | The most seconds the session may run.                                                                                                   |
| `parks_at`     | timestamp         | When the phone will park itself: `last_active_at` plus `idle_timeout`, or the session's deadline if that comes first.                   |
| `park_reason`  | string            | Which rule sets `parks_at`: `idle` or `max_duration`. Until the phone is ready, the idle timer hasn't started, so it is `max_duration`. |
| `reserved_usd` | money             | The credit held for the session, renewals included.                                                                                     |

## Create a phone [#create-a-phone]

`POST /v1/phones` needs the `phones:create` scope.

Creates a phone and opens its first session, so billing starts at once. The body is optional, and a request without one creates a phone with every default. A key limited to certain phones can't create phones.

| Header            | Rules                                                                                                                                    |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `Idempotency-Key` | Optional, and recommended on every create. 8 to 128 characters: letters, digits, `_`, `.`, `:` and `-`, starting with a letter or digit. |

| Field          | Type    | Default        | Rules                                                                                                                                                                                                                                                        |
| -------------- | ------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`         | string  | The phone's ID | 1 to 80 characters, after surrounding spaces are trimmed.                                                                                                                                                                                                    |
| `country`      | string  | Any country    | One of the codes that [`GET /v1/account`](/docs/api-reference/account) lists in `countries`, such as `DE`. It is where the phone appears to be. Any other code is refused with `400 validation_failed` before a phone is made, so nothing is held or billed. |
| `metadata`     | object  | `{}`           | Up to 20 pairs. A key has 1 to 40 characters, which are letters, digits, `_`, `:` and `-`, and it starts with a letter or digit. A value is a string of up to 256 characters.                                                                                |
| `idle_timeout` | integer | 300            | Seconds, from 60 to 3600.                                                                                                                                                                                                                                    |
| `max_duration` | integer | 900            | Shortens to fit available credit when omitted. Seconds, from 60 to 10800.                                                                                                                                                                                    |
| `wait`         | boolean | `true`         | Whether to wait, for up to about 50 seconds, until the phone is ready.                                                                                                                                                                                       |

```json title="POST /v1/phones"
{
  "name": "checkout-test",
  "metadata": { "customer": "acme" },
  "idle_timeout": 300,
  "max_duration": 1800
}
```

```bash
key="checkout-test-$(date +%s)"   # a new key for each phone; send the same one to repeat this create
curl https://phonebox.dev/v1/phones \
  -H "Authorization: Bearer $PHONEBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $key" \
  -d '{"name": "checkout-test", "metadata": {"customer": "acme"}, "idle_timeout": 300, "max_duration": 1800}'
```

Both successful answers carry the phone, with its path in the `Location` header:

* `201 Created` means the phone is ready.
* `202 Accepted` means the phone was created but isn't ready yet. Either `wait` was `false`, the wait of about 50 seconds ran out, or the wait itself failed. Poll `GET /v1/phones/{id}?wait=ready` while the phone is `creating` or `starting`. Any other status ends the loop: `ready` means you can use the phone, and any other means you read its `status` and `failure`, as [Get a phone](/docs/api-reference/phones#get-a-phone) describes. If your key was revoked in the meantime, the poll answers `401 key_revoked`.

```json title="Response: phone"
{
  "id": "ph_7kx2m6q4v3ta",
  "object": "phone",
  "name": "checkout-test",
  "status": "creating",
  "country": null,
  "metadata": { "customer": "acme" },
  "created_at": "2026-09-29T10:00:00.412Z",
  "last_active_at": null,
  "session": {
    "id": "ses_q2w6e4r5t3y7",
    "started_at": "2026-09-29T10:00:00.412Z",
    "ready_at": null,
    "idle_timeout": 300,
    "max_duration": 1800,
    "parks_at": "2026-09-29T10:30:00.412Z",
    "park_reason": "max_duration",
    "reserved_usd": "1.800000"
  },
  "failure": null
}
```

A repeat with the same `Idempotency-Key` and body within 24 hours answers with the phone the first request created, as it is now: `201` when it is ready, and `202` otherwise. That includes a phone that has since failed, which comes back with `status` set to `failed` and its `failure`: create the next phone with a new key.

When the phone fails while the request waits, the answer is that failure as an error, with the phone's ID in `details.phone`. The phone won't recover, so the error has `retryable` set to `false` and no `Retry-After`, and its `next` ends by telling you to create a new phone, with a new idempotency key if you use one. Examples are `503 capacity_unavailable` when no device could be found for it, and `400 validation_failed` whose `details.issues` names `country` when no phones are available in the country you asked for. A failure before the phone was ever ready costs nothing.

| Error                       | When                                                                                                                                                                                                                                                       |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 validation_failed`     | A body field or the `Idempotency-Key` breaks its rules. `details.issues` lists each problem.                                                                                                                                                               |
| `413 payload_too_large`     | The body is larger than 100 KB.                                                                                                                                                                                                                            |
| `403 insufficient_scope`    | The key lacks `phones:create`, or it is limited to certain phones. `details.required` is `phones:create`.                                                                                                                                                  |
| `409 idempotency_conflict`  | The `Idempotency-Key` was used in the last 24 hours with a different body. `wait` isn't compared. `details.phone` names the phone the key created, and `next` says to send the original body with that key, because a new key would create a second phone. |
| `402 project_frozen`        | The project is frozen. Contact support.                                                                                                                                                                                                                    |
| `503 service_paused`        | Phonebox is paused for maintenance.                                                                                                                                                                                                                        |
| `429 rate_limited`          | The project made 60 lifecycle requests this minute.                                                                                                                                                                                                        |
| `409 running_limit_reached` | The project already runs its maximum number of phones, which `details.limit` gives.                                                                                                                                                                        |
| `409 phone_limit_reached`   | The project already has its maximum number of phones, not counting deleted or failed ones. `details.limit` gives it.                                                                                                                                       |
| `503 capacity_unavailable`  | No phones are free right now. Try again after `Retry-After`.                                                                                                                                                                                               |
| `402 insufficient_credits`  | Your balance, minus what is reserved, can't cover the reservation. `details.max_affordable_seconds` is the longest `max_duration` you can afford now.                                                                                                      |
| `402 spend_limit_reached`   | The reservation would take this month's spending past the monthly limit.                                                                                                                                                                                   |

The admission checks run in the order of this table, from `project_frozen` down.

## List phones [#list-phones]

`GET /v1/phones` needs the `phones:read` scope.

Lists the project's phones, newest first.

| Parameter       | Type    | Default                                 | Rules                                                                                                  |
| --------------- | ------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `status`        | string  | Every status but `failed` and `deleted` | One status. `?status=failed` lists failed phones, and `?status=deleted` deleted ones.                  |
| `metadata[key]` | string  | None                                    | One metadata pair to match, such as `metadata[customer]=acme`. The key follows the metadata key rules. |
| `limit`         | integer | 50                                      | From 1 to 100.                                                                                         |
| `cursor`        | string  | None                                    | The `next_cursor` of the previous page.                                                                |

```bash
curl -g "https://phonebox.dev/v1/phones?status=parked&metadata[customer]=acme" \
  -H "Authorization: Bearer $PHONEBOX_API_KEY"
```

curl needs `-g` to send the brackets as they are.

```json title="Response: phones"
{
  "data": [
    {
      "id": "ph_3nq5w2z7c4hd",
      "object": "phone",
      "name": "acme-support",
      "status": "parked",
      "country": null,
      "metadata": { "customer": "acme" },
      "created_at": "2026-09-29T12:40:07.118Z",
      "last_active_at": "2026-09-29T12:58:41.036Z",
      "session": null,
      "failure": null
    },
    {
      "id": "ph_7kx2m6q4v3ta",
      "object": "phone",
      "name": "checkout-test",
      "status": "parked",
      "country": null,
      "metadata": { "customer": "acme" },
      "created_at": "2026-09-29T10:00:00.412Z",
      "last_active_at": "2026-09-29T10:12:27.506Z",
      "session": null,
      "failure": null
    }
  ],
  "next_cursor": null
}
```

A key limited to certain phones gets every one of its phones that matches, on one page. For such a key, `limit` and `cursor` have no effect, and `next_cursor` is always `null`.

| Error                   | When                                                                                                                                                              |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 validation_failed` | An unknown `status`, a `limit` outside 1 to 100, a parameter given twice, more than one metadata pair, a malformed metadata key, or an invalid or expired cursor. |

## Get a phone [#get-a-phone]

`GET /v1/phones/{id}` needs the `phones:read` scope.

Returns the phone. With `wait`, it long-polls until the phone reaches a status.

| Parameter | Type    | Default | Rules                                                                |
| --------- | ------- | ------- | -------------------------------------------------------------------- |
| `wait`    | string  | None    | `ready` or `parked`: wait until the phone has that status.           |
| `timeout` | integer | 50      | The most seconds to wait, from 1 to 55. It applies only with `wait`. |

```bash
curl "https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta?wait=ready&timeout=50" \
  -H "Authorization: Bearer $PHONEBOX_API_KEY"
```

A wait answers `200 OK` with the phone as soon as it has the status you asked for. It answers early when the phone is in a status it can't leave for that one without another request: `parking`, `parked`, `unavailable`, `failed` or `deleted` for `ready`, and `failed` or `deleted` for `parked`. Otherwise it answers when `timeout` runs out, with the phone as it is.

Poll again only while the phone is on its way: `creating` or `starting` when you wait for `ready`, and `parking` when you wait for `parked`. Any other status ends the loop, so read `status` and `failure` and act on them:

* `failed` or `deleted`: the phone can't be used again, so create a new one, with a new idempotency key if you use one.
* `parked` or `unavailable`, when you waited for `ready`: the phone isn't running, so start it. When `failure` shows that a start failed, start it again later rather than at once.
* `parking`, when you waited for `ready`: wait with `?wait=parked`, then start it.
* `ready` or `starting`, when you waited for `parked`: someone started the phone again, so park it again if you still mean to. An `unavailable` phone isn't billed, so there is nothing to wait for.

```json title="Response: phone"
{
  "id": "ph_7kx2m6q4v3ta",
  "object": "phone",
  "name": "checkout-test",
  "status": "ready",
  "country": null,
  "metadata": { "customer": "acme" },
  "created_at": "2026-09-29T10:00:00.412Z",
  "last_active_at": "2026-09-29T10:01:04.771Z",
  "session": {
    "id": "ses_q2w6e4r5t3y7",
    "started_at": "2026-09-29T10:00:00.412Z",
    "ready_at": "2026-09-29T10:01:04.771Z",
    "idle_timeout": 300,
    "max_duration": 1800,
    "parks_at": "2026-09-29T10:06:04.771Z",
    "park_reason": "idle",
    "reserved_usd": "1.800000"
  },
  "failure": null
}
```

If the phone fails during the wait, the answer is that failure as an error, with the phone's ID in `details.phone`, as for create. A phone that had failed before the poll began comes back as it is, with `status` set to `failed` and its `failure`.

A `wait=parked` poll doesn't count as activity, so it never keeps the phone from parking.

| Error                                     | When                                                               |
| ----------------------------------------- | ------------------------------------------------------------------ |
| `400 validation_failed`                   | `wait` isn't `ready` or `parked`, or `timeout` is outside 1 to 55. |
| The phone's failure, with `details.phone` | The phone failed during the wait.                                  |

## Update a phone [#update-a-phone]

`PATCH /v1/phones/{id}` needs the `phones:control` scope.

Renames a phone or replaces its metadata, in any status but `deleted`. The body is optional, and a field you leave out stays as it is.

| Field      | Type   | Default   | Rules                                                                                                                                   |
| ---------- | ------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `name`     | string | Unchanged | 1 to 80 characters, after surrounding spaces are trimmed.                                                                               |
| `metadata` | object | Unchanged | Replaces all of the phone's metadata, so send every pair you want to keep. `{}` removes them all. The rules are the same as for create. |

```json title="PATCH /v1/phones/{id}"
{
  "metadata": { "customer": "acme", "plan": "trial" }
}
```

```bash
curl -X PATCH https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta \
  -H "Authorization: Bearer $PHONEBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"metadata": {"customer": "acme", "plan": "trial"}}'
```

```json title="Response: phone"
{
  "id": "ph_7kx2m6q4v3ta",
  "object": "phone",
  "name": "checkout-test",
  "status": "parked",
  "country": null,
  "metadata": { "customer": "acme", "plan": "trial" },
  "created_at": "2026-09-29T10:00:00.412Z",
  "last_active_at": "2026-09-29T10:12:27.506Z",
  "session": null,
  "failure": null
}
```

| Error                   | When                            |
| ----------------------- | ------------------------------- |
| `400 validation_failed` | A field breaks its rules.       |
| `413 payload_too_large` | The body is larger than 100 KB. |
| `409 phone_deleted`     | The phone was deleted.          |

## Delete a phone [#delete-a-phone]

`DELETE /v1/phones/{id}` needs the `phones:delete` scope, which only Admin keys have.

Destroys the phone's device with every app, file and signed-in account on it. This can't be undone. An open session ends with the end reason `deleted` and is billed up to that moment, with the one-minute minimum, even if the phone was never ready. The answer is the phone with `status` set to `deleted`, and deleting it again returns the same.

```bash
curl -X DELETE https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta \
  -H "Authorization: Bearer $PHONEBOX_API_KEY"
```

```json title="Response: phone"
{
  "id": "ph_7kx2m6q4v3ta",
  "object": "phone",
  "name": "checkout-test",
  "status": "deleted",
  "country": null,
  "metadata": { "customer": "acme", "plan": "trial" },
  "created_at": "2026-09-29T10:00:00.412Z",
  "last_active_at": "2026-09-29T10:12:27.506Z",
  "session": null,
  "failure": null
}
```

| Error                    | When                                                               |
| ------------------------ | ------------------------------------------------------------------ |
| `403 insufficient_scope` | The key isn't an Admin key. `details.required` is `phones:delete`. |
| `429 rate_limited`       | The project made 60 lifecycle requests this minute.                |

## Start a phone [#start-a-phone]

`POST /v1/phones/{id}/start` needs the `phones:control` scope.

Starts a `parked` or `unavailable` phone: it opens a new session, which reserves credit for `max_duration`, and resumes the device with its apps, files and sign-ins. On a running phone, one that is `creating`, `starting` or `ready`, it renews the session instead: the deadline moves to `max_duration` from now, only the extra time is reserved, and a ready phone's idle timer restarts. The body is optional.

| Field          | Type    | Default                     | Rules                                                                  |
| -------------- | ------- | --------------------------- | ---------------------------------------------------------------------- |
| `idle_timeout` | integer | The phone's current setting | Seconds, from 60 to 3600.                                              |
| `max_duration` | integer | The phone's current setting | Seconds, from 60 to 10800.                                             |
| `wait`         | boolean | `true`                      | Whether to wait, for up to about 50 seconds, until the phone is ready. |

The timers you give become the phone's settings, so later starts keep them.

```json title="POST /v1/phones/{id}/start"
{
  "idle_timeout": 900,
  "max_duration": 7200
}
```

```bash
curl https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta/start \
  -H "Authorization: Bearer $PHONEBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"idle_timeout": 900, "max_duration": 7200}'
```

It answers `200 OK` with the ready phone, or `202 Accepted` with the phone still `starting`, in which case you poll `GET /v1/phones/{id}?wait=ready` while it is `starting`.

```json title="Response: phone"
{
  "id": "ph_7kx2m6q4v3ta",
  "object": "phone",
  "name": "checkout-test",
  "status": "starting",
  "country": null,
  "metadata": { "customer": "acme" },
  "created_at": "2026-09-29T10:00:00.412Z",
  "last_active_at": "2026-09-29T10:12:27.506Z",
  "session": {
    "id": "ses_x7c3v5b2n6m4",
    "started_at": "2026-09-29T14:20:02.118Z",
    "ready_at": null,
    "idle_timeout": 900,
    "max_duration": 7200,
    "parks_at": "2026-09-29T16:20:02.118Z",
    "park_reason": "max_duration",
    "reserved_usd": "7.200000"
  },
  "failure": null
}
```

When the start fails while the request waits, the answer is that failure as an error, with the phone's ID in `details.phone`. A phone that doesn't become ready within 5 minutes of a start parks again with its data, and its `failure` says why; that error keeps its code's usual `retryable`, because you can start the phone again. A phone whose device is lost becomes `failed` instead, and its error isn't retryable: create a new phone, with a new idempotency key if you use one. If the wait fails for any other reason, such as a key revoked in the meantime, the start has already happened, so the request answers `202 Accepted` with the phone as it was last seen.

| Error                                     | When                                                                                                                                          |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 validation_failed`                   | A field breaks its rules.                                                                                                                     |
| `413 payload_too_large`                   | The body is larger than 100 KB.                                                                                                               |
| `409 phone_parking`                       | The phone is still parking. This error is retryable: wait with `?wait=parked`, then start it.                                                 |
| `409 phone_failed`, `409 phone_deleted`   | The phone can't be used again. Create a new one, with a new idempotency key if you use one.                                                   |
| `402 project_frozen`                      | The project is frozen. Contact support.                                                                                                       |
| `503 service_paused`                      | Phonebox is paused for maintenance.                                                                                                           |
| `429 rate_limited`                        | The project made 60 lifecycle requests this minute.                                                                                           |
| `409 running_limit_reached`               | A new start would pass the running limit, which `details.limit` gives.                                                                        |
| `503 capacity_unavailable`                | No phones are free for a new start. Try again after `Retry-After`.                                                                            |
| `402 insufficient_credits`                | Your available balance can't cover the reservation, or a renewal's extra time. `details.max_affordable_seconds` says how much you can afford. |
| `402 spend_limit_reached`                 | The reservation would take this month's spending past the monthly limit.                                                                      |
| The phone's failure, with `details.phone` | The phone failed to start during the wait.                                                                                                    |

## Park a phone [#park-a-phone]

`POST /v1/phones/{id}/park` needs the `phones:control` scope.

Parks a running phone: the session closes and billing stops at once, the phone becomes `parking`, and it becomes `parked` once its device has stopped. A phone that isn't running is returned as it is, and a phone that is already parking is waited on. The body is optional.

| Field  | Type    | Default | Rules                                                                   |
| ------ | ------- | ------- | ----------------------------------------------------------------------- |
| `wait` | boolean | `true`  | Whether to wait, for up to about 50 seconds, until the phone is parked. |

```bash
curl -X POST https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta/park \
  -H "Authorization: Bearer $PHONEBOX_API_KEY"
```

It answers `200 OK` with the parked phone, or `202 Accepted` with the phone still `parking`, in which case you can poll `GET /v1/phones/{id}?wait=parked` while it is `parking`. When the wait fails for any reason but the phone's own failure, the answer is `202` too, with the phone as it was last seen. Billing has stopped either way.

```json title="Response: phone"
{
  "id": "ph_7kx2m6q4v3ta",
  "object": "phone",
  "name": "checkout-test",
  "status": "parked",
  "country": null,
  "metadata": { "customer": "acme" },
  "created_at": "2026-09-29T10:00:00.412Z",
  "last_active_at": "2026-09-29T14:41:18.520Z",
  "session": null,
  "failure": null
}
```

| Error                                     | When                                                                                                                                                                 |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 validation_failed`                   | `wait` isn't a boolean, or the body has another field.                                                                                                               |
| `413 payload_too_large`                   | The body is larger than 100 KB.                                                                                                                                      |
| `409 phone_failed`, `409 phone_deleted`   | The phone can't be used again. Create a new one, with a new idempotency key if you use one.                                                                          |
| `429 rate_limited`                        | The project made 60 lifecycle requests this minute.                                                                                                                  |
| The phone's failure, with `details.phone` | The phone failed while it parked, for example because its device was lost. The error isn't retryable: create a new phone, with a new idempotency key if you use one. |

## Keep a phone awake [#keep-a-phone-awake]

`POST /v1/phones/{id}/heartbeat` needs the `phones:control` scope.

Restarts a ready phone's idle timer and returns the phone. It takes no body and doesn't move the session's deadline: [start the phone](/docs/api-reference/phones#start-a-phone) to renew it. A phone that is still `creating` or `starting` is returned as it is.

```bash
curl -X POST https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta/heartbeat \
  -H "Authorization: Bearer $PHONEBOX_API_KEY"
```

```json title="Response: phone"
{
  "id": "ph_7kx2m6q4v3ta",
  "object": "phone",
  "name": "checkout-test",
  "status": "ready",
  "country": null,
  "metadata": { "customer": "acme" },
  "created_at": "2026-09-29T10:00:00.412Z",
  "last_active_at": "2026-09-29T14:32:50.264Z",
  "session": {
    "id": "ses_x7c3v5b2n6m4",
    "started_at": "2026-09-29T14:20:02.118Z",
    "ready_at": "2026-09-29T14:20:41.309Z",
    "idle_timeout": 900,
    "max_duration": 7200,
    "parks_at": "2026-09-29T14:47:50.264Z",
    "park_reason": "idle",
    "reserved_usd": "7.200000"
  },
  "failure": null
}
```

| Error                                   | When                                                                                        |
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
| `409 phone_parking`                     | The phone is parking. This error is retryable: wait with `?wait=parked`.                    |
| `409 phone_not_running`                 | The phone is parked or unavailable, as `details.status` says.                               |
| `409 phone_failed`, `409 phone_deleted` | The phone can't be used again. Create a new one, with a new idempotency key if you use one. |
