# Device

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

> Read the phone's device info, read and set the clipboard, location, locale and timezone, and reboot or reset a phone.



These requests read or change the phone itself. Each needs a `ready` phone. Reads need the `phones:read` scope, except the clipboard's, which needs `phones:control` like every change; a reset needs `phones:delete`. [Device settings](/docs/using-phones/device) shows them in context.

Some phones don't offer every feature on this page. A request for one the phone doesn't offer fails with `422 feature_unavailable`, and `details.feature` names it, such as `device_info`, `location`, `timezone`, `locale` or `reset`. Nothing was sent to the phone.

Each one that writes is sent to the phone once and never retried. When the phone doesn't confirm it, the request fails with `502 action_outcome_unknown`: read the setting back, or observe, before you send it again. The readiness and phone-service errors in [Common errors](/docs/api-reference#common-errors) apply to every request on this page.

## Get device info [#get-device-info]

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

The phone's brand, model, Android version, screen size in device pixels and mobile carrier. Any of them is `null` when the phone doesn't report it. Nothing else about the phone's hardware or identity is returned.

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

```json title="Response: device"
{
  "brand": "google",
  "model": "Pixel 9",
  "android_version": "15",
  "screen": { "width": 1080, "height": 2424, "density": 420 },
  "carrier": "T-Mobile"
}
```

## Read the clipboard [#read-the-clipboard]

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

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

```json title="Response: clipboard"
{
  "text": "Order 58213 confirmed"
}
```

| Error                       | When                                                                                                                                                                                                                                                   |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `409 clipboard_unavailable` | The phone's default text input was switched off, so its clipboard can't be read. Switch it back in the phone's Settings, or read the text on screen with [observe](/docs/api-reference/observe#observe-the-screen). Setting the clipboard still works. |

## Set the clipboard [#set-the-clipboard]

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

| Field  | Type   | Default | Rules                                            |
| ------ | ------ | ------- | ------------------------------------------------ |
| `text` | string | None    | 1 to 10,000 characters. Whitespace is preserved. |

Clearing the clipboard with empty text is not supported. Sending `{"text":""}` returns
`400 validation_failed` before any phone action runs. Do not retry an empty write or restart the phone to clear it.

```json title="PUT /v1/phones/{id}/clipboard"
{
  "text": "https://example.com/reset?code=7F3K9Q"
}
```

```bash
curl -X PUT https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta/clipboard \
  -H "Authorization: Bearer $PHONEBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "https://example.com/reset?code=7F3K9Q"}'
```

The answer repeats the text. Activity records it only as `[redacted]`.

```json title="Response: clipboard"
{
  "text": "https://example.com/reset?code=7F3K9Q"
}
```

| Error                   | When                                                                            |
| ----------------------- | ------------------------------------------------------------------------------- |
| `400 validation_failed` | `text` is missing, empty or longer than 10,000 characters. No phone action ran. |

## Read the location [#read-the-location]

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

The location the phone reports to apps. Until you set one, it is the phone's default, New York.

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

```json title="Response: current location"
{
  "lat": 40.7128,
  "lng": -74.006
}
```

## Set the location [#set-the-location]

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

Sets the location that the phone reports to apps and, by default, the phone's timezone to the one at that location, so its clock matches the place. The timezone comes from the nearest large place, or, far out at sea, from the longitude; near a border it can be the neighbour's, so check `timezone` in the answer and [set the timezone](#set-the-timezone) yourself if it is wrong.

| Field                    | Type    | Default | Rules                                                                                  |
| ------------------------ | ------- | ------- | -------------------------------------------------------------------------------------- |
| `lat`                    | number  | None    | Latitude in decimal degrees, from -90 to 90.                                           |
| `lng`                    | number  | None    | Longitude in decimal degrees, from -180 to 180.                                        |
| `timezone_from_location` | boolean | `true`  | Also set the timezone found at the location. Send `false` to change only the location. |

```json title="PUT /v1/phones/{id}/location"
{
  "lat": 40.748817,
  "lng": -73.985428
}
```

```bash
curl -X PUT https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta/location \
  -H "Authorization: Bearer $PHONEBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"lat": 40.748817, "lng": -73.985428}'
```

The answer names the timezone it set, or `null` when `timezone_from_location` was `false`, or the phone can't change its timezone or refused the one found.

```json title="Response: location"
{
  "lat": 40.748817,
  "lng": -73.985428,
  "timezone": "America/New_York"
}
```

The location and the timezone are two changes, each sent once. When the timezone fails after the location was set, the error has `details.location_set: true` and `details.timezone`, and `next` is the `PUT /v1/phones/{id}/timezone` request that sets it.

| Error                     | When                                       |
| ------------------------- | ------------------------------------------ |
| `400 validation_failed`   | `lat` or `lng` is missing or out of range. |
| `422 feature_unavailable` | The phone can't change its location.       |

## Reset the location [#reset-the-location]

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

Goes back to the phone's default location, New York, and, when the phone can change its timezone, to the timezone found there, so its clock matches the place as after [Set the location](#set-the-location). The two changes are each sent once. When the timezone fails after the location was reset, the error has `details.location_reset: true` and `details.timezone`, and `next` is the `PUT /v1/phones/{id}/timezone` request that sets it.

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

```json title="Response: reset location"
{
  "reset": true
}
```

## Read the locale [#read-the-locale]

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

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

```json title="Response: current locale"
{
  "locale": "en-US"
}
```

## Set the locale [#set-the-locale]

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

Changes the phone's language and region. The phone's interface restarts, so observe again before you act, and expect its labels in the new language.

| Field    | Type   | Default | Rules                                                                                                                                                                                                               |
| -------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `locale` | string | None    | A language code of 2 or 3 lowercase letters, then an optional script such as `Hant`, then an optional region: two capital letters or a 3-digit area code. Examples are `en-US`, `de-DE`, `zh-Hant-TW` and `es-419`. |

```json title="PUT /v1/phones/{id}/locale"
{
  "locale": "fr-FR"
}
```

```bash
curl -X PUT https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta/locale \
  -H "Authorization: Bearer $PHONEBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"locale": "fr-FR"}'
```

```json title="Response: locale"
{
  "locale": "fr-FR"
}
```

| Error                   | When                                            |
| ----------------------- | ----------------------------------------------- |
| `400 validation_failed` | `locale` is missing or doesn't have that shape. |

## Read the timezone [#read-the-timezone]

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

The phone's IANA timezone, or `null` while it uses its default.

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

```json title="Response: current timezone"
{
  "timezone": "Europe/Paris"
}
```

## Set the timezone [#set-the-timezone]

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

| Field      | Type   | Default | Rules                                                                                                                            |
| ---------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `timezone` | string | None    | An IANA time zone name, spelled exactly, such as `Europe/Paris` or `America/New_York`. Offsets such as `+05:30` aren't accepted. |

```json title="PUT /v1/phones/{id}/timezone"
{
  "timezone": "Europe/Paris"
}
```

```bash
curl -X PUT https://phonebox.dev/v1/phones/ph_7kx2m6q4v3ta/timezone \
  -H "Authorization: Bearer $PHONEBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"timezone": "Europe/Paris"}'
```

```json title="Response: timezone"
{
  "timezone": "Europe/Paris"
}
```

| Error                   | When                                             |
| ----------------------- | ------------------------------------------------ |
| `400 validation_failed` | `timezone` is missing or isn't a time zone name. |

## Reboot [#reboot]

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

Restarts the phone, keeping everything on it. It takes no body and answers `202 Accepted` with the phone, now `starting`, which has to prove it is ready again, as after a start. Wait with `GET /v1/phones/{id}?wait=ready` before you use it. The session stays open, so the reboot's time is billed like any other.

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

```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-29T14:26:13.402Z",
  "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:41:13.402Z",
    "park_reason": "idle",
    "reserved_usd": "7.200000"
  },
  "failure": null
}
```

A phone can reboot once every 10 minutes. A second reboot within that time fails with `429 rate_limited`, and both `details.retry_after_seconds` and the `Retry-After` header give the seconds left:

```json title="Response: error"
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limited",
    "message": "Too many requests. Wait a moment and try again.",
    "retryable": true,
    "next": "A phone can reboot once every 10 minutes.",
    "request_id": "req_n6p2r4s7t3v5",
    "details": { "retry_after_seconds": 347 }
  }
}
```

When the phone service refuses a reboot outright, the phone didn't restart, and the 10-minute allowance isn't used up.

| Error                        | When                                                                                                                                                                                 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `429 rate_limited`           | The phone rebooted less than 10 minutes ago.                                                                                                                                         |
| `502 action_outcome_unknown` | The phone service didn't confirm the reboot, so the phone may be restarting. `next` is `GET /v1/phones/{id}?wait=ready`: wait for ready, and never send a second reboot to find out. |

## Reset [#reset]

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

Wipes the phone back to a clean state: its installed apps and their data, its files, its signed-in accounts and its clipboard are deleted, and can't be brought back. The phone keeps its ID and its session. It takes no body and answers `202 Accepted` with the phone, now `starting`, and `wiped`, which lists what was deleted. The phone comes back in about a minute: wait with `GET /v1/phones/{id}?wait=ready` before you use it. The session stays open, so the reset's time is billed like any other.

The reset is sent once and never retried. A second reset while the phone is still starting fails with `409 phone_starting`, and one within a minute of the last fails with `429 rate_limited`.

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

```json title="Response: reset"
{
  "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-29T14:26:13.402Z",
    "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:41:13.402Z",
      "park_reason": "idle",
      "reserved_usd": "7.200000"
    },
    "failure": null
  },
  "wiped": ["apps", "files", "accounts", "clipboard"]
}
```

| Error                        | When                                                                                                                                                                              |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `403 insufficient_scope`     | The key doesn't have `phones:delete`. Use an Admin key.                                                                                                                           |
| `422 feature_unavailable`    | The phone can't be reset. Delete it and create a new one instead.                                                                                                                 |
| `502 action_outcome_unknown` | The phone service didn't confirm the reset, so the phone may be resetting. `next` is `GET /v1/phones/{id}?wait=ready`: wait for ready, and never send a second reset to find out. |
