Phonebox / Docs
Interfaces

REST API

The base URL, authentication, JSON conventions, errors, request IDs, idempotency, pagination, waiting and rate limits.

View as Markdown

The REST API is the interface everything else is built on: the CLI, the SDK and the MCP server all call it. Its base URL is https://phonebox.dev/v1, and the OpenAPI 3.1 document at /openapi.json describes every endpoint.

Requests

Authenticate every request with a project API key as a bearer token, and call the API from a server. Only the two terminal setup requests take no key. A request that carries an Origin header, as every browser request does, fails with 403 browser_requests_not_allowed. Authentication and keys covers keys and scopes.

Authorization: Bearer pbx_…
Content-Type: application/json
  • Send JSON bodies with Content-Type: application/json. Most bodies may be up to 100 KB, and an action batch up to 200 KB. A file upload is the exception: its body is the file's raw bytes, up to 4 MB.
  • Bodies are checked strictly, so a field the endpoint doesn't know fails with 400 validation_failed instead of being ignored.
  • The bodies of create, start, park and live links are optional. Leave them out to take every default.
  • Give each query parameter at most once.

Timestamps are ISO 8601 in UTC. Durations are in seconds, except inside actions, where ms, duration_ms and timeout_ms are in milliseconds. Money is a string of US dollars with six decimal places, such as "0.060000", so that no amount is rounded.

Endpoints

MethodPathScopeWhat it does
POST/v1/phonesphones:createCreates a phone.
GET/v1/phonesphones:readLists phones, newest first.
GET/v1/phones/{id}phones:readGets a phone, or waits for a status with wait.
PATCH/v1/phones/{id}phones:controlChanges a phone's name or metadata.
DELETE/v1/phones/{id}phones:deleteDeletes a phone permanently.
POST/v1/phones/{id}/startphones:controlStarts a parked phone, or renews a running one.
POST/v1/phones/{id}/parkphones:controlParks a phone.
POST/v1/phones/{id}/heartbeatphones:controlResets a phone's idle timer.
GET/v1/phones/{id}/sessionsphones:readLists a phone's sessions with their cost.
GET/v1/phones/{id}/observephones:readReads the screen.
GET/v1/phones/{id}/screenshotphones:readReturns a screenshot image.
POST/v1/phones/{id}/actionsphones:controlRuns a batch of actions.
GET/v1/apps/libraryphones:readSearches the app library: the apps a phone installs by package.
GET/v1/phones/{id}/appsphones:readLists installed apps.
POST/v1/phones/{id}/appsphones:controlInstalls an app from Phonebox's app library, or your own upload, in the background.
GET/v1/phones/{id}/apps/installsphones:readLists recent installs.
DELETE/v1/phones/{id}/apps/{package}phones:controlUninstalls an app.
POST/v1/apps/uploadsphones:controlCreates an upload of your own APK, and the URL its file goes to.
GET/v1/apps/uploadsphones:readLists the project's uploads.
GET/v1/apps/uploads/{id}phones:readGets an upload, or waits until it is processed.
POST/v1/apps/uploads/{id}/completephones:controlStarts processing an upload once its file has arrived.
DELETE/v1/apps/uploads/{id}phones:controlDeletes an upload.
GET/v1/phones/{id}/filesphones:readLists a directory.
PUT/v1/phones/{id}/filesphones:controlUploads a file.
GET/v1/phones/{id}/files/contentphones:readDownloads a file.
DELETE/v1/phones/{id}/filesphones:controlDeletes a file. Folders are deleted in the phone’s Files app.
GET/v1/phones/{id}/devicephones:readGets the brand, model, Android version, screen and carrier.
GET/v1/phones/{id}/clipboardphones:controlReads the clipboard.
PUT/v1/phones/{id}/clipboardphones:controlSets the clipboard.
GET/v1/phones/{id}/locationphones:readReads the location.
PUT/v1/phones/{id}/locationphones:controlSets the location and, by default, the timezone found there.
DELETE/v1/phones/{id}/locationphones:controlResets the location.
GET/v1/phones/{id}/localephones:readReads the locale.
PUT/v1/phones/{id}/localephones:controlSets the locale.
GET/v1/phones/{id}/timezonephones:readReads the timezone.
PUT/v1/phones/{id}/timezonephones:controlSets the timezone.
POST/v1/phones/{id}/rebootphones:controlReboots the phone.
POST/v1/phones/{id}/resetphones:deleteWipes the phone's apps, files, accounts and clipboard, keeping its ID.
POST/v1/phones/{id}/recordingsphones:controlStarts recording the screen.
GET/v1/phones/{id}/recordingsphones:readLists the phone's recordings.
POST/v1/phones/{id}/recordings/{rid}/stopphones:controlStops a recording.
GET/v1/phones/{id}/recordings/{rid}/videophones:readDownloads a recording's video.
DELETE/v1/phones/{id}/recordings/{rid}phones:controlDeletes a recording.
POST/v1/phones/{id}/livephones:controlCreates a live view link.
GET/v1/accountphones:readReturns the balance, limits and price.
POST/v1/credits/checkoutphones:controlCreates a checkout link where a person buys credits.
POST/v1/setupNo keyStarts terminal setup: a code for a person to confirm in the browser.
POST/v1/setup/tokenNo keyReturns the new key once the person has confirmed.

Every request that reaches into the phone, which means observing, screenshots, actions, apps, files, device settings and live links, needs the phone to be ready. For a phone in any other status, it fails with the reason and a next step, such as 409 phone_not_running with "next": "POST /v1/phones/ph_7kx2m6q4v3ta/start".

Responses

Responses are JSON, except the text form of observe, screenshots and file downloads. Every response, errors included, carries these headers:

HeaderMeaning
X-Request-IdThe request's ID, such as req_5f7a2c4d6e3b.
Cache-ControlAlways no-store.

Include the request ID when you contact support about a request. The console's activity page shows your requests by the same IDs.

A successful request answers 200 OK. A created phone, an uploaded file, a new live link and a new recording answer 201 Created. A phone that is still creating, starting or parking when a wait ends answers 202 Accepted, and so do an app install, a reboot and a reset, which continue in the background.

Errors

Every error has the same envelope:

Response: error
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_failed",
    "message": "The request is invalid.",
    "retryable": false,
    "next": null,
    "request_id": "req_k4m2n7p3q5r6",
    "details": {
      "issues": [{ "path": ["idle_timeout"], "message": "Too big: expected number to be <=3600" }]
    }
  }
}
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 don't parse it.
retryableWhether the same request may succeed later without changes. A failed phone's own failure, which names the phone in details.phone, is never retryable, and neither is an internal_error on a request other than a GET, because its write may have taken effect.
nextThe next step, such as the request to make, when there is a useful one. Otherwise null.
request_idThe same ID as the X-Request-Id header.
detailsFacts about this error, such as issues for a validation error or phone for a failed phone.

The HTTP status follows the code:

StatusCodes
400validation_failed, with details.issues naming each problem
401invalid_api_key, key_expired, key_revoked
402insufficient_credits, spend_limit_reached, project_frozen
403insufficient_scope, phone_not_allowed, browser_requests_not_allowed
404phone_not_found, file_not_found, app_not_found, and not_found for a path that doesn't exist
405method_not_allowed, with an Allow header listing the path's methods
409phone_not_running, phone_starting, phone_parking, phone_deleted, phone_failed, idempotency_conflict, install_in_progress, running_limit_reached, phone_limit_reached
413payload_too_large
422app_not_available
429rate_limited, with Retry-After
500internal_error
502provider_error, action_outcome_unknown
503capacity_unavailable with Retry-After (a failed phone's own failure has none), phone_unavailable, service_paused
504provider_timeout

An action that fails inside a batch reports its error in its own result, in the same shape without type and request_id, while the batch itself answers 200. Those results use further codes, such as target_not_found and wait_timeout. Actions and targets lists them.

A phone that doesn't exist and a phone in another project both answer 404 phone_not_found, so a key learns nothing about other projects.

Idempotency

Idempotency-Key makes a create safe to repeat. The key holds 8 to 128 characters, which are letters, digits, _, ., : and -, and it starts with a letter or digit. A repeat with the same key and the same body within 24 hours returns the phone the first request created, as it is now. If that phone has failed, or was deleted, a repeat only returns it again, so create the next phone with a new key. A repeat with a different body fails with 409 idempotency_conflict, which names the phone the key created in details.phone: send the original body with that key to get it, because a new key would create a second phone. wait isn't compared, so one key works across the REST API, MCP, the SDK and the CLI, whether each of them waits or not.

Every POST checks the format of an Idempotency-Key it is given, but only create acts on one. Park and heartbeat are safe to repeat as they are. A start on a running phone renews its session and reserves credit again, so read the phone before you repeat one. An action batch is never replayed, so a repeated batch runs again. Don't repeat one unless Phonebox's error says nothing ran.

Pagination

GET /v1/phones and GET /v1/phones/{id}/sessions return a page:

Response: phones
{
  "data": [
    {
      "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
    }
  ],
  "next_cursor": "Q1pXk7Rb2mTn8VwLc4Hy9sJf3GdA6uEo"
}

limit sets the page size, from 1 to 100, with a default of 50. To get the next page, send next_cursor back as cursor. next_cursor is null on the last page. Treat cursors as opaque: an invalid or expired one fails with 400 validation_failed.

GET /v1/phones lists phones newest first and leaves out failed and deleted ones, which can't be used again, unless you ask for them with status. It takes two filters:

  • status, such as ?status=ready;
  • one metadata pair, such as ?metadata[customer]=acme.

A key limited to certain phones gets all of its phones on one page.

Waiting

POST /v1/phones, POST /v1/phones/{id}/start and POST /v1/phones/{id}/park wait for the phone by default, and GET /v1/phones/{id}?wait=ready or ?wait=parked waits on request. Every wait answers within about 50 seconds. A poll's timeout goes up to 55 seconds, with a default of 50. When the wait ends before the phone gets there, the answer is 202 from a lifecycle request, or the phone in its current status from a poll. Poll again while the phone is creating or starting for ready, or parking for parked. Any other status ends the loop: read the phone's status and failure and act on them.

The limit exists because a waiting request sends nothing until it answers, and many proxies and load balancers close a connection that stays silent for 60 seconds. Waits answer within about 50 seconds, but an action batch can take much longer: it starts no new action after 90 seconds, an action that has started still runs to its end, and a request that runs a batch may take up to 300 seconds at the platform. Give your HTTP client a timeout of about 300 seconds for batches, as the SDK does with 310, because a client that gives up sooner can leave a batch's outcome unknown. For other requests, about 130 seconds is enough.

Lifecycle and timers describes every wait in detail.

Retries

  • Phonebox retries its own reads of the phone for up to about 15 seconds, which covers the short restarts a phone can have right after it becomes ready. You may repeat any read.
  • When an error's retryable is true, the same request may succeed later. Wait for Retry-After when the response has one, or a few seconds when it doesn't. For a write, retryable doesn't mean that the first attempt did nothing, so follow the rules below.
  • Repeat a create only with the same Idempotency-Key, never without one. After a create whose phone failed or was deleted, create the next phone with a new key.
  • Phonebox never retries an action, and neither should your code, because an action might have taken effect. A batch refused with any error in Phonebox's error envelope except 500 internal_error ran nothing, so you may send it again once you've dealt with the error, as When nothing ran explains. In a batch that ran, an action result whose error says the action didn't run, such as phone_unavailable with retryable set to true, lets you send that action and the ones after it again. Otherwise, observe the screen and decide.
  • Only an answer in Phonebox's error envelope says what happened. A 502 or 504 without it, which a proxy or the hosting platform sends, a timeout or a dropped connection leaves the outcome unknown. Repeat reads, starts, parks and heartbeats, repeat a create only with the same Idempotency-Key, and observe before you repeat anything else. Failures without the envelope has the details.

Rate limits

LimitScope
600 requests a minuteEach API key, across all its requests.
60 lifecycle operations a minuteEach project, across creates, starts, parks and deletes.
1 reboot every 10 minutesEach phone.
1 reset a minuteEach phone.

A request over a limit fails with 429 rate_limited and a Retry-After header in seconds. When the wait is known exactly, as for a reboot, details.retry_after_seconds says it too. Phonebox counts the request limit on each of its servers, so treat it as approximate, while the lifecycle limit is exact.

On this page