Phonebox / Docs
API reference

Errors

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

View as Markdown

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

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" }
  }
}
FieldMeaning
typeThe 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.
codeThe exact error. Branch on this.
messageA sentence for people. It can change, so never parse it.
retryableWhether 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.
nextThe next step, or null.
request_idThe request's ID, the same as its X-Request-Id header.
detailsFacts 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 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

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.

CodeHTTPTypeRetryableMessageWhat to do
validation_failed400invalid_request_errorfalseThe 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_request400invalid_request_errorfalseThe request is invalid.The MCP server refuses a JSON-RPC batch with this code. Send one message per request.
invalid_api_key401authentication_errorfalseThe API key is missing or invalid.Send Authorization: Bearer pbx_… with a whole key from the console.
key_expired401authentication_errorfalseThis API key has expired.Create a new key in the console.
key_revoked401authentication_errorfalseThis 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.
unauthorized401authentication_errorfalseSign in to continue.On the API, this means Phonebox's own servers are misconfigured. Contact support with the request ID.
insufficient_credits402billing_errorfalseThere 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_reached402billing_errorfalseThis session would exceed the project's monthly spending limit.Raise the monthly spending limit in Settings, or ask for a shorter max_duration.
project_frozen402billing_errorfalseThis 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_scope403permission_errorfalseThis 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_allowed403permission_errorfalseThis key is restricted to other phones.Use a key that may use this phone.
browser_requests_not_allowed403permission_errorfalseCall 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_denied403permission_errorfalseThis setup request was declined in the browser.The person chose Cancel on the confirmation page. Start terminal setup again only if they ask you to.
not_found404not_found_errorfalseThe resource was not found.No endpoint exists at this path. Check it against the endpoint list.
phone_not_found404not_found_errorfalseNo 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_found404not_found_errorfalseNo file exists at this path.List the parent directory to see what is at details.path.
app_not_found404not_found_errorfalseThis 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_found404not_found_errorfalseNo 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_found404not_found_errorfalseNo 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_allowed405invalid_request_errorfalseThis method isn't supported on this path.Use a method from the Allow header.
phone_not_running409conflict_errorfalseThe phone isn't running.Start the phone with the request in next. details.status is parked or unavailable.
phone_starting409conflict_errortrueThe phone is still starting.Wait with GET /v1/phones/{id}?wait=ready, then send the request again.
phone_parking409conflict_errortrueThe phone is parking.Wait with GET /v1/phones/{id}?wait=parked, then start the phone.
phone_deleted409conflict_errorfalseThe phone was deleted.Create a new phone. A deleted phone and its data can't be brought back.
phone_failed409conflict_errorfalseThe 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_conflict409conflict_errorfalseThis 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_progress409conflict_errortrueThe 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_downgrade409conflict_errorfalseA 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_ready409conflict_errorfalseThis 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_reached409conflict_errorfalseThe 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_unavailable409conflict_errorfalseThe 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_progress409conflict_errorfalseA 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_ready409conflict_errortrueThis 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_reached409conflict_errorfalseThe 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_upload409conflict_errorfalseThis 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_expired409conflict_errorfalseThis setup request expired. Start setup again.The code wasn't confirmed, or the key wasn't collected, within 15 minutes. Start terminal setup again and show the new link and code.
setup_used409conflict_errorfalseThis 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_limit409conflict_errorfalseThe 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_reached409conflict_errorfalseThe 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. details.limit is the current one, which counts all of your projects together.
phone_limit_reached409conflict_errorfalseThe 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_large413invalid_request_errorfalseThe 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_found422invalid_request_errorfalseNo element on the screen matches the target.Observe, then use one of details.candidates, or scroll to bring the element into view.
ambiguous_target422invalid_request_errorfalseSeveral 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_snapshot422invalid_request_errorfalseThe screen changed since that snapshot was taken.Observe again and use the new refs.
no_focused_field422invalid_request_errorfalseNo text field is focused on the screen.Tap the text field, then type.
wait_timeout422invalid_request_errortrueThe condition wasn't met before the timeout.Observe. If the screen is still loading, wait again with a longer timeout_ms.
app_not_available422invalid_request_errorfalseThis 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_apk422invalid_request_errorfalseThis 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_failed422invalid_request_errorfalseThe 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_unavailable422invalid_request_errorfalseThis 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_limited429rate_limit_errortrueToo 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_error502provider_errortrueThe 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_unknown502provider_errorfalseThe phone didn't confirm this action.Observe before anything else, and never repeat the action blindly. See Unknown outcomes.
upload_failed502provider_errortrueThe 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_unavailable503provider_errortrueNo 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_unavailable503provider_errortrueThe 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_paused503api_errortruePhonebox is paused for maintenance.Try again later. Your phones and everything on them are kept.
storage_full503api_errortruePhonebox 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_configured503api_errortruePayments 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_configured503api_errortrueThe platform isn't configured yet.Phonebox is missing a setting. Try again later, and contact support if it lasts.
provider_timeout504provider_errortrueThe 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_error500api_errortrueSomething 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

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 lists what each one means for the batch.

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

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 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

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 shows both.

On this page