# Errors

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

> The error envelope, what next, details and retryable mean, and every code the API and the MCP server can return.



Every error that Phonebox sends has the same envelope, whatever the endpoint, and the HTTP status follows its code:

```json title="Response: error"
{
  "error": {
    "type": "conflict_error",
    "code": "phone_not_running",
    "message": "The phone isn't running.",
    "retryable": false,
    "next": "POST /v1/phones/ph_7kx2m6q4v3ta/start",
    "request_id": "req_e2f5g3h6j4k7",
    "details": { "status": "parked" }
  }
}
```

| Field        | Meaning                                                                                                                                                                                                              |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`       | The error's family: `authentication_error`, `permission_error`, `invalid_request_error`, `not_found_error`, `conflict_error`, `billing_error`, `rate_limit_error`, `provider_error` or `api_error`.                  |
| `code`       | The exact error. Branch on this.                                                                                                                                                                                     |
| `message`    | A sentence for people. It can change, so never parse it.                                                                                                                                                             |
| `retryable`  | Whether the same request may succeed later without any change. A failed phone's own failure, which names the phone in `details.phone` while the phone's `status` is `failed`, is never retryable, whatever its code. |
| `next`       | The next step, or `null`.                                                                                                                                                                                            |
| `request_id` | The request's ID, the same as its `X-Request-Id` header.                                                                                                                                                             |
| `details`    | Facts about this error, or `{}`.                                                                                                                                                                                     |

`next` names the step that moves you forward when there is a useful one, often the exact request to make, such as `POST /v1/phones/ph_7kx2m6q4v3ta/start`, and it is `null` otherwise. `details` holds machine-readable facts about this particular error, such as `issues`, `phone`, `required`, `candidates` or `retry_after_seconds`, and a code may gain new fields in it over time.

A failure without this envelope doesn't come from Phonebox, and it leaves the outcome unknown, as [Failures without the envelope](/docs/api-reference/errors#failures-without-the-envelope) explains.

For `validation_failed`, the message also names the first invalid field. `details.issues` includes `path`, `code` and `message`, plus expected types, received types, bounds or union `alternatives` when available. `details.validation_stage` identifies the check that rejected the request. Correct the fields before sending again; request validation failures ran no phone actions. Save the request ID when reporting a server error. SDK/CLI validation before a request is sent has no request ID.

## Codes [#codes]

These are all the codes the API and the MCP server can return. A code you don't recognize may be added later, so treat it by its HTTP status and `retryable`.

| Code                           | HTTP | Type                    | Retryable | Message                                                          | What to do                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------ | ---- | ----------------------- | --------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `validation_failed`            | 400  | `invalid_request_error` | `false`   | The request is invalid.                                          | Fix each problem that `details.issues` lists as `{path, message}`, then send the request again. With `details.phone`, the phone service refused the phone you asked for, usually its `country`. That phone has failed, so this error isn't retryable: create a new phone, with a new idempotency key if you use one, without that `country` or with another.                              |
| `invalid_request`              | 400  | `invalid_request_error` | `false`   | The request is invalid.                                          | The MCP server refuses a JSON-RPC batch with this code. Send one message per request.                                                                                                                                                                                                                                                                                                     |
| `invalid_api_key`              | 401  | `authentication_error`  | `false`   | The API key is missing or invalid.                               | Send `Authorization: Bearer pbx_…` with a whole key from the console.                                                                                                                                                                                                                                                                                                                     |
| `key_expired`                  | 401  | `authentication_error`  | `false`   | This API key has expired.                                        | Create a new key in the console.                                                                                                                                                                                                                                                                                                                                                          |
| `key_revoked`                  | 401  | `authentication_error`  | `false`   | This API key was revoked.                                        | Use another key. If you didn't revoke this one, check the console's activity for calls you don't recognize.                                                                                                                                                                                                                                                                               |
| `unauthorized`                 | 401  | `authentication_error`  | `false`   | Sign in to continue.                                             | On the API, this means Phonebox's own servers are misconfigured. Contact support with the request ID.                                                                                                                                                                                                                                                                                     |
| `insufficient_credits`         | 402  | `billing_error`         | `false`   | There aren't enough credits to reserve this session.             | Add credits with `phonebox setup` or in the console, or ask for a shorter `max_duration`, at most `details.max_affordable_seconds`.                                                                                                                                                                                                                                                       |
| `spend_limit_reached`          | 402  | `billing_error`         | `false`   | This session would exceed the project's monthly spending limit.  | Raise the monthly spending limit in Settings, or ask for a shorter `max_duration`.                                                                                                                                                                                                                                                                                                        |
| `project_frozen`               | 402  | `billing_error`         | `false`   | This project is frozen. Contact support.                         | Contact support. A refund that leaves too little credit for running phones, or a review of how the project is used, freezes it.                                                                                                                                                                                                                                                           |
| `insufficient_scope`           | 403  | `permission_error`      | `false`   | This key doesn't have permission for this action.                | Use a key with the scope that `details.required` names. A key limited to certain phones can't create phones.                                                                                                                                                                                                                                                                              |
| `phone_not_allowed`            | 403  | `permission_error`      | `false`   | This key is restricted to other phones.                          | Use a key that may use this phone.                                                                                                                                                                                                                                                                                                                                                        |
| `browser_requests_not_allowed` | 403  | `permission_error`      | `false`   | Call the API from a server, not a browser.                       | Call Phonebox from your server, and give a person a live view link instead of a key.                                                                                                                                                                                                                                                                                                      |
| `setup_denied`                 | 403  | `permission_error`      | `false`   | This setup request was declined in the browser.                  | The person chose Cancel on the confirmation page. Start [terminal setup](/docs/api-reference/setup) again only if they ask you to.                                                                                                                                                                                                                                                        |
| `not_found`                    | 404  | `not_found_error`       | `false`   | The resource was not found.                                      | No endpoint exists at this path. Check it against the [endpoint list](/docs/api-reference#endpoints).                                                                                                                                                                                                                                                                                     |
| `phone_not_found`              | 404  | `not_found_error`       | `false`   | No phone with this ID exists in the project.                     | Check the ID in `details.phone`. A phone in another project answers the same way.                                                                                                                                                                                                                                                                                                         |
| `file_not_found`               | 404  | `not_found_error`       | `false`   | No file exists at this path.                                     | List the parent directory to see what is at `details.path`.                                                                                                                                                                                                                                                                                                                               |
| `app_not_found`                | 404  | `not_found_error`       | `false`   | This app isn't installed on the phone.                           | Install the app, or check the package name in `details.package`. An install that hasn't finished answers this too.                                                                                                                                                                                                                                                                        |
| `upload_not_found`             | 404  | `not_found_error`       | `false`   | No upload with this ID exists in the project.                    | Check the ID in `details.upload`. An upload in another project answers the same way.                                                                                                                                                                                                                                                                                                      |
| `recording_not_found`          | 404  | `not_found_error`       | `false`   | No recording with this ID exists for this phone.                 | Check the ID with `GET /v1/phones/{id}/recordings`. Recordings are deleted 7 days after they start, and an ID from another phone is no recording of this one.                                                                                                                                                                                                                             |
| `method_not_allowed`           | 405  | `invalid_request_error` | `false`   | This method isn't supported on this path.                        | Use a method from the `Allow` header.                                                                                                                                                                                                                                                                                                                                                     |
| `phone_not_running`            | 409  | `conflict_error`        | `false`   | The phone isn't running.                                         | Start the phone with the request in `next`. `details.status` is `parked` or `unavailable`.                                                                                                                                                                                                                                                                                                |
| `phone_starting`               | 409  | `conflict_error`        | `true`    | The phone is still starting.                                     | Wait with `GET /v1/phones/{id}?wait=ready`, then send the request again.                                                                                                                                                                                                                                                                                                                  |
| `phone_parking`                | 409  | `conflict_error`        | `true`    | The phone is parking.                                            | Wait with `GET /v1/phones/{id}?wait=parked`, then start the phone.                                                                                                                                                                                                                                                                                                                        |
| `phone_deleted`                | 409  | `conflict_error`        | `false`   | The phone was deleted.                                           | Create a new phone. A deleted phone and its data can't be brought back.                                                                                                                                                                                                                                                                                                                   |
| `phone_failed`                 | 409  | `conflict_error`        | `false`   | The phone failed and can't be used. Create a new phone.          | Create a new phone, with a new idempotency key if you use one: the old key returns this phone. A failed phone holds no device, costs nothing from then on, and doesn't count toward your phone limit.                                                                                                                                                                                     |
| `idempotency_conflict`         | 409  | `conflict_error`        | `false`   | This Idempotency-Key was already used with a different request.  | `details.phone` names the phone the key created. Send the original body with that key to get it back. A new key would create a second phone, so use one only when you want another phone.                                                                                                                                                                                                 |
| `install_in_progress`          | 409  | `conflict_error`        | `true`    | The phone can't start another install yet.                       | Another install is still running on this phone, which runs at most two at once, and one per app. Installs take a few seconds, so send it again shortly, a few times at most. `GET /v1/phones/{id}/apps/installs` shows when the running install has finished, or failed.                                                                                                                  |
| `version_downgrade`            | 409  | `conflict_error`        | `false`   | A newer version of this app is installed on the phone.           | The phone has `details.installed_version_code` of the app, newer than the upload's `details.version_code`. Build with a higher `versionCode`, or install with `replace: true`, which uninstalls the app first and deletes its data.                                                                                                                                                       |
| `upload_not_ready`             | 409  | `conflict_error`        | `false`   | This upload isn't ready to install.                              | `details.status` says why. Wait for a `processing` upload with `GET /v1/apps/uploads/{id}?wait=ready`, complete an `awaiting_file` one, and upload the APK again for any other status. A `ready` upload that answers this with `retryable: true` is being prepared for the phone: send the install again in a minute.                                                                     |
| `storage_limit_reached`        | 409  | `conflict_error`        | `false`   | The project's uploads already use all of its storage.            | Delete uploads you no longer need with `DELETE /v1/apps/uploads/{id}`. `details.limit_bytes` is the project's limit, and `details.used_bytes` what its uploads hold.                                                                                                                                                                                                                      |
| `clipboard_unavailable`        | 409  | `conflict_error`        | `false`   | The phone can't read its clipboard right now.                    | 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 `GET /v1/phones/{id}/observe`. Setting the clipboard still works.                                                                                                                                                                |
| `recording_in_progress`        | 409  | `conflict_error`        | `false`   | A recording is already running on this phone.                    | A phone records one video at a time. Find the running one with `GET /v1/phones/{id}/recordings` and stop it with `POST /v1/phones/{id}/recordings/{rid}/stop`, then start again.                                                                                                                                                                                                          |
| `recording_not_ready`          | 409  | `conflict_error`        | `true`    | This recording's video isn't ready yet.                          | Stop the recording if it is still running, then list the phone's recordings until its `status` is `ready`, and download it again.                                                                                                                                                                                                                                                         |
| `recording_limit_reached`      | 409  | `conflict_error`        | `false`   | The project already keeps 20 recordings.                         | Delete a recording you no longer need with `DELETE /v1/phones/{id}/recordings/{rid}`, on any of the project's phones, or wait for one to expire 7 days after it started. A deleted phone's recordings count until they expire.                                                                                                                                                            |
| `duplicate_upload`             | 409  | `conflict_error`        | `false`   | This file is already one of the project's uploads.               | An upload's `error` has this code when its file is another upload of the project, which is ready: `next` names that upload, and completing answered with it. Install that upload.                                                                                                                                                                                                         |
| `setup_expired`                | 409  | `conflict_error`        | `false`   | This setup request expired. Start setup again.                   | The code wasn't confirmed, or the key wasn't collected, within 15 minutes. Start [terminal setup](/docs/api-reference/setup) again and show the new link and code.                                                                                                                                                                                                                        |
| `setup_used`                   | 409  | `conflict_error`        | `false`   | This setup request already delivered its key. Start setup again. | A setup request hands out its key once. Use the key you stored. If its answer was lost, start setup again and revoke the lost key in the console.                                                                                                                                                                                                                                         |
| `key_limit`                    | 409  | `conflict_error`        | `false`   | The project already has 50 active keys.                          | Terminal setup couldn't add a key to the project the person chose. Revoke a key you no longer use on the console's API keys page, then start setup again.                                                                                                                                                                                                                                 |
| `running_limit_reached`        | 409  | `conflict_error`        | `false`   | The project already has its maximum number of running phones.    | Park only a phone you created for this task and no longer need, never another agent's. Otherwise stop and tell your user, who can ask us for a higher limit at [team@phonebox.dev](mailto:team@phonebox.dev). `details.limit` is the current one, which counts all of your projects together.                                                                                             |
| `phone_limit_reached`          | 409  | `conflict_error`        | `false`   | The project already has its maximum number of phones.            | Delete only a phone you created and no longer need. In a project that several agents share, tell your user instead, or ask us for a higher limit. `details.limit` is the current one.                                                                                                                                                                                                     |
| `payload_too_large`            | 413  | `invalid_request_error` | `false`   | The request body is too large.                                   | Send less: at most 100 KB of JSON, 200 KB for an action batch, and 4 MB for a file. A file download over 4 MB fails the same way. An upload's `error` has this code, with the message "The file is larger than an upload takes.", when its APK is over 500 MiB: upload a smaller build.                                                                                                   |
| `target_not_found`             | 422  | `invalid_request_error` | `false`   | No element on the screen matches the target.                     | Observe, then use one of `details.candidates`, or scroll to bring the element into view.                                                                                                                                                                                                                                                                                                  |
| `ambiguous_target`             | 422  | `invalid_request_error` | `false`   | Several elements on the screen match the target.                 | Target a candidate by `ref` with `details.snapshot_id`, or add `nth` to a text target. Only text targets take `nth`, so narrow an ID or description target instead.                                                                                                                                                                                                                       |
| `stale_snapshot`               | 422  | `invalid_request_error` | `false`   | The screen changed since that snapshot was taken.                | Observe again and use the new refs.                                                                                                                                                                                                                                                                                                                                                       |
| `no_focused_field`             | 422  | `invalid_request_error` | `false`   | No text field is focused on the screen.                          | Tap the text field, then type.                                                                                                                                                                                                                                                                                                                                                            |
| `wait_timeout`                 | 422  | `invalid_request_error` | `true`    | The condition wasn't met before the timeout.                     | Observe. If the screen is still loading, wait again with a longer `timeout_ms`.                                                                                                                                                                                                                                                                                                           |
| `app_not_available`            | 422  | `invalid_request_error` | `false`   | This app isn't available to install.                             | The app isn't in Phonebox's app library, where an install by package looks; it never reaches Google Play. Check the package name, or install the app from the Play Store app on the phone.                                                                                                                                                                                                |
| `invalid_apk`                  | 422  | `invalid_request_error` | `false`   | This file isn't an APK Phonebox can install.                     | An upload's `error` has this code when its file isn't an APK Phonebox can read, and `next` says why. Build an APK, for example with `./gradlew assembleDebug`, and upload that. An Android App Bundle (`.aab`) or a split APK can't be installed on its own.                                                                                                                              |
| `install_failed`               | 422  | `invalid_request_error` | `false`   | The phone couldn't install this app.                             | An install by upload failed on the phone: its row in `GET /v1/phones/{id}/apps/installs` has this code, and `next` says what to try. When the phone already had the app, a build signed with another key is the usual cause: uninstall the app, or install with `replace: true`.                                                                                                          |
| `feature_unavailable`          | 422  | `invalid_request_error` | `false`   | This phone doesn't support this feature.                         | `details.feature` names the feature this phone doesn't offer, such as `recordings`, `reset` or `timezone`. Nothing was sent to the phone. Carry on without it.                                                                                                                                                                                                                            |
| `rate_limited`                 | 429  | `rate_limit_error`      | `true`    | Too many requests. Wait a moment and try again.                  | Wait for `Retry-After`, then try again. A reboot's limit and the phone service's also give the wait in `details.retry_after_seconds`.                                                                                                                                                                                                                                                     |
| `provider_error`               | 502  | `provider_error`        | `true`    | The phone service returned an error.                             | Send a read again after a few seconds. After a write, observe the phone before you send it again.                                                                                                                                                                                                                                                                                         |
| `action_outcome_unknown`       | 502  | `provider_error`        | `false`   | The phone didn't confirm this action.                            | Observe before anything else, and never repeat the action blindly. See [Unknown outcomes](/docs/troubleshooting/unknown-outcomes).                                                                                                                                                                                                                                                        |
| `upload_failed`                | 502  | `provider_error`        | `true`    | The upload couldn't be stored.                                   | An upload's `error` has this code when Phonebox couldn't store its file. Create a new upload and send the file again.                                                                                                                                                                                                                                                                     |
| `capacity_unavailable`         | 503  | `provider_error`        | `true`    | No phones are available right now.                               | Try again after `Retry-After`. With `details.phone`, that phone couldn't get a device. A new phone has then failed, and the error is `retryable: false` with no `Retry-After`: create a new phone, with a new idempotency key if you use one. A started phone parked again instead, and the error stays retryable: start it again later.                                                  |
| `phone_unavailable`            | 503  | `provider_error`        | `true`    | The phone is temporarily unavailable.                            | Nothing was done on the phone. Try again after `Retry-After`, or after a few seconds when there is none. With `details.phone`, the phone itself became unavailable: start it again later.                                                                                                                                                                                                 |
| `service_paused`               | 503  | `api_error`             | `true`    | Phonebox is paused for maintenance.                              | Try again later. Your phones and everything on them are kept.                                                                                                                                                                                                                                                                                                                             |
| `storage_full`                 | 503  | `api_error`             | `true`    | Phonebox can't store more apps right now.                        | Phonebox's app storage is full. Creating an upload answers it, and completing one does too, keeping the file for the rest of the upload's hour so that completing it again later works. An upload's `error` has this code when the storage filled while it was processing: try again later with a new upload. Contact support if it lasts.                                                |
| `billing_not_configured`       | 503  | `api_error`             | `true`    | Payments aren't connected yet.                                   | Credits can't be bought on this deployment yet, so no checkout link was created. Try again later, and contact support if it lasts.                                                                                                                                                                                                                                                        |
| `platform_not_configured`      | 503  | `api_error`             | `true`    | The platform isn't configured yet.                               | Phonebox is missing a setting. Try again later, and contact support if it lasts.                                                                                                                                                                                                                                                                                                          |
| `provider_timeout`             | 504  | `provider_error`        | `true`    | The phone didn't respond in time.                                | With `details.phone`, the phone didn't become ready in time: a new phone failed, so the error isn't retryable and you create another, with a new idempotency key if you use one, and a started phone parked again, so you start it again. In an action result, the batch ran out of time: observe, then send the actions that didn't run, which `details.not_run` counts, in a new batch. |
| `internal_error`               | 500  | `api_error`             | `true`    | Something went wrong on our side.                                | Keep the request ID. A GET's error is retryable, so send the read again. On any other request, the write may have taken effect, so the error has `retryable` set to `false` and `next` says to read the phone before you retry. Contact support if it keeps happening.                                                                                                                    |

## Errors inside a batch [#errors-inside-a-batch]

An action that fails inside a batch reports its error in its own result, with `code`, `message`, `retryable`, `next` and `details`, while the batch itself answers `200`. Codes such as `target_not_found` and `wait_timeout` appear only there. [Action errors](/docs/api-reference/actions#action-errors) lists what each one means for the batch.

## Waiting with Retry-After [#waiting-with-retry-after]

A `429 rate_limited` and a `503 capacity_unavailable` carry a `Retry-After` header in seconds: the limit's own wait when it has one, and 30 seconds otherwise. A failed phone's own failure carries none, because it isn't retryable. A `503 phone_unavailable` carries one only when the wait is known, which is 2 seconds when the phone's control service was briefly out of reach. Other errors carry none.

## Failures without the envelope [#failures-without-the-envelope]

Only an answer in this envelope, with its `error.code`, comes from Phonebox, so only such an answer says what happened to your request. Anything else leaves the outcome unknown:

* a `502` or `504` whose body isn't this JSON, which a proxy, a load balancer or the hosting platform sends, for example when a request runs past its time limit;
* a timeout on your side;
* a dropped connection, or no answer at all.

The request may have finished, partly finished or never started. Send a read again. Send a create again only with the same `Idempotency-Key`, and a park or heartbeat as it was, since they are safe to repeat. Read the phone before you send a start again: if it is `starting` or `ready`, it is running, and another start would renew its session, reserving credit again. Before you send any other write again, such as an action batch, an install or a file upload, observe the phone to find out whether it happened, as [Unknown outcomes](/docs/troubleshooting/unknown-outcomes#when-no-answer-comes-from-phonebox) describes.

The SDK and the CLI report an answer without the envelope as `internal_error`, with its HTTP status in `status`. They read a body as Phonebox's envelope only when the answer carries an `X-Request-Id` header, which every answer from Phonebox has, so a proxy's JSON error isn't taken for one. The CLI reports a failed connection or a timeout as `internal_error` too, while the SDK throws the error that `fetch` gave it, such as a `TypeError` or a `TimeoutError`.

## Errors over MCP [#errors-over-mcp]

The MCP server answers a refused tool call with `isError` set to `true` and this same envelope as its content. MCP has no headers, so a `Retry-After` wait appears as `details.retry_after_seconds`. Arguments that don't match a tool's schema are refused in the MCP library's own shape instead. [MCP](/docs/interfaces/mcp#errors) shows both.
