Phonebox / Docs
API reference

Conventions

The conventions every API request follows, from authentication and idempotency to pagination, rate limits and money, and the list of endpoints.

View as Markdown

The API lives at https://phonebox.dev/v1. This section documents every endpoint exactly: its method and path, the scope it needs, its parameters with their types, defaults and ranges, its responses and the errors it can return. The OpenAPI 3.1 document at /openapi.json describes the same endpoints for tools and code generators. For a guided tour, read REST API and Using phones first.

Endpoints

OperationMethod and pathScope
Create a phonePOST /v1/phonesphones:create
List phonesGET /v1/phonesphones:read
Get a phoneGET /v1/phones/{id}phones:read
Update a phonePATCH /v1/phones/{id}phones:control
Delete a phoneDELETE /v1/phones/{id}phones:delete
Start a phonePOST /v1/phones/{id}/startphones:control
Park a phonePOST /v1/phones/{id}/parkphones:control
Keep a phone awakePOST /v1/phones/{id}/heartbeatphones:control
List a phone's sessionsGET /v1/phones/{id}/sessionsphones:read
Observe the screenGET /v1/phones/{id}/observephones:read
Take a screenshotGET /v1/phones/{id}/screenshotphones:read
Run actionsPOST /v1/phones/{id}/actionsphones:control
Search the app libraryGET /v1/apps/libraryphones:read
List appsGET /v1/phones/{id}/appsphones:read
Install an appPOST /v1/phones/{id}/appsphones:control
List app installsGET /v1/phones/{id}/apps/installsphones:read
Uninstall an appDELETE /v1/phones/{id}/apps/{package}phones:control
Create an uploadPOST /v1/apps/uploadsphones:control
List uploadsGET /v1/apps/uploadsphones:read
Get an uploadGET /v1/apps/uploads/{id}phones:read
Complete an uploadPOST /v1/apps/uploads/{id}/completephones:control
Delete an uploadDELETE /v1/apps/uploads/{id}phones:control
List filesGET /v1/phones/{id}/filesphones:read
Upload a filePUT /v1/phones/{id}/filesphones:control
Download a fileGET /v1/phones/{id}/files/contentphones:read
Delete a fileDELETE /v1/phones/{id}/filesphones:control
Get device infoGET /v1/phones/{id}/devicephones:read
Read the clipboardGET /v1/phones/{id}/clipboardphones:control
Set the clipboardPUT /v1/phones/{id}/clipboardphones:control
Read the locationGET /v1/phones/{id}/locationphones:read
Set the locationPUT /v1/phones/{id}/locationphones:control
Reset the locationDELETE /v1/phones/{id}/locationphones:control
Read the localeGET /v1/phones/{id}/localephones:read
Set the localePUT /v1/phones/{id}/localephones:control
Read the timezoneGET /v1/phones/{id}/timezonephones:read
Set the timezonePUT /v1/phones/{id}/timezonephones:control
RebootPOST /v1/phones/{id}/rebootphones:control
ResetPOST /v1/phones/{id}/resetphones:delete
Start a recordingPOST /v1/phones/{id}/recordingsphones:control
List recordingsGET /v1/phones/{id}/recordingsphones:read
Stop a recordingPOST /v1/phones/{id}/recordings/{rid}/stopphones:control
Download a recordingGET /v1/phones/{id}/recordings/{rid}/videophones:read
Delete a recordingDELETE /v1/phones/{id}/recordings/{rid}phones:control
Create a live view linkPOST /v1/phones/{id}/livephones:control
Get the accountGET /v1/accountphones:read
Create a checkout linkPOST /v1/credits/checkoutphones:control
Start terminal setupPOST /v1/setupNo key
Poll terminal setupPOST /v1/setup/tokenNo key

{id} is a phone ID such as ph_7kx2m6q4v3ta, and {package} is an Android package name such as org.wikipedia.

Authentication

Send a project API key as a bearer token with every request, except the two terminal setup requests, which are how a terminal gets its key:

Authorization: Bearer pbx_…

A key is pbx_ followed by 43 characters, and it selects its project, so you never send a project ID. Each endpoint needs one scope. Agent keys have phones:read, phones:control and phones:create, Admin keys add phones:delete, and Read-only keys have only phones:read. A key limited to certain phones sees and uses only those phones, and it never has phones:create. Authentication and keys covers presets, limits and expiry.

Call the API from a server or an agent, never from a web page. Phonebox refuses any request that carries an Origin header with 403 browser_requests_not_allowed.

Content types

  • Send a JSON body with Content-Type: application/json. A body without that header, or one that isn't valid JSON, fails with 400 validation_failed.
  • A JSON body may be up to 100 KB, and an action batch up to 200 KB. A larger body fails with 413 payload_too_large.
  • A file upload is the exception: its body is the file's raw bytes, up to 4 MB.
  • Bodies are checked strictly. A field the endpoint doesn't define fails with 400 validation_failed instead of being ignored.
  • Create, update, start, park and live links take an optional body. Leave it out to take every default.
  • Give each query parameter at most once. A repeated one fails with 400 validation_failed.

Responses are JSON, with three exceptions: the text form of observe is text/plain, a screenshot is image/png or image/jpeg, and a file download is application/octet-stream.

Request IDs

Every response, errors included, carries an X-Request-Id header, such as req_k4m2n7p3q5r6, and Cache-Control: no-store. An error repeats the ID in its request_id field. The console's Activity page lists your calls by the same IDs, so quote the ID when you contact support.

An MCP tool call's response carries the ID of the first REST request the tool made, so that ID finds the call in Activity too.

Idempotency

Send an Idempotency-Key header with every create. Without one, a create whose reply was lost can't be told apart from a create that failed, and sending it again can create, and bill, a second phone.

  • A key holds 8 to 128 characters: letters, digits, _, ., : and -, starting with a letter or digit. Generate a new one for each phone you mean to create, and store it before you send the request.
  • A repeat with the same key and the same body within 24 hours returns the phone that the first request created, as it is now. It creates nothing, reserves nothing and doesn't count toward the lifecycle rate limit. The order of keys in the body doesn't matter.
  • If that phone has failed, or was deleted, a repeat only returns it again. Create the next phone with a new key.
  • A repeat with the same key and a different body fails with 409 idempotency_conflict. 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. The conflict names the phone the key created in details.phone: send the original body with that key to get it, never a new key, which would create a second phone.
  • 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 without a key. Start isn't: on a running phone it renews the session and reserves credit again, so read the phone before you send a start again, as the next of a write's internal_error says.
  • An action batch is never replayed, so a repeated batch runs again. Run actions says when a repeat is safe.

In the SDK, the option is idempotencyKey, and the MCP tool create_phone takes idempotency_key. Once create_phone has created a phone, it returns that phone even when a later wait fails.

Pagination

GET /v1/phones and GET /v1/phones/{id}/sessions return pages of the shape {"data": […], "next_cursor": …}, newest first.

ParameterTypeDefaultRules
limitinteger50From 1 to 100 items.
cursorstringNoneThe next_cursor of the previous page, sent back unchanged.

next_cursor is null on the last page. Treat cursors as opaque. An invalid or expired cursor fails with 400 validation_failed.

A key limited to certain phones gets all of its matching phones from GET /v1/phones on one page: limit and cursor have no effect there, and next_cursor is always null.

Waiting

Create, start and park wait for the phone by default, for up to about 50 seconds. GET /v1/phones/{id}?wait=ready or ?wait=parked waits on request, for its timeout: from 1 to 55 seconds, 50 by default.

A waiting request sends nothing until it answers, and many proxies and load balancers close a connection that stays silent for 60 seconds. That is why no wait is longer. After a 202 Accepted from create or start, poll GET /v1/phones/{id}?wait=ready while the phone is creating or starting. After one from park, poll ?wait=parked while it is parking. Any other status ends the loop: go on when the phone has the status you waited for, and otherwise read its status and failure and act on them, as Get a phone describes. A wait=ready poll answers at once for a phone in any other status, so polling again would only spin.

The MCP server's waiting tools are never silent for longer either. Without a progressToken they answer within about 50 seconds, and with one they keep waiting for up to about 100 seconds, sending progress notifications as they go. MCP explains both.

Give your HTTP client a timeout above 60 seconds, so that it outlasts every wait. Give action batches about 300 seconds; the SDK uses 310. A batch's waits alone may add up to 60 seconds, its actions take time of their own, and it stops starting new actions only after 90 seconds, but an action that has started runs to its end, so a request that runs a batch may take up to 300 seconds at the platform.

Rate limits

LimitApplies to
600 requests a minuteEach API key, across all its requests. Phonebox counts it on each of its servers, so treat it as approximate.
60 lifecycle requests a minuteEach project: creates, starts and renewals, parks and deletes, repeats included. A replayed create doesn't count. This limit is exact.
1 reboot every 10 minutesEach phone.
10 checkout links an hourEach project. This limit is exact.
10 setup requests a minuteEach IP address. Approximate, like the key limit.
60 setup polls a minuteEach setup request. Approximate, like the key limit.
1 reset a minuteEach phone. A reset also has to wait until the phone is ready again.
20 recordingsEach project. Each is deleted 7 days after it started.

A request over a limit fails with 429 rate_limited and a Retry-After header in seconds. The header holds the limit's own wait when it has one, such as the time left before a phone can reboot again, which details.retry_after_seconds repeats. Otherwise it is 30 seconds.

Timestamps, durations and money

  • Timestamps are ISO 8601 strings in UTC with milliseconds, such as 2026-09-29T10:00:00.412Z.
  • Durations are whole seconds, such as idle_timeout and max_duration. Inside actions, ms, duration_ms and timeout_ms are milliseconds.
  • Money is a string of US dollars with exactly six decimal places, one for each millionth of a dollar, such as "0.060000" for the price of a minute. Parse it as a decimal, never as a floating-point number.

Retries

  • Phonebox retries its own reads of a phone for up to about 15 seconds, which rides out the short restarts a phone can have right after it becomes ready. You may repeat any read.
  • Phonebox never retries a write, because a write might have taken effect. When a write couldn't reach the phone at all, it fails with Phonebox's retryable 503 phone_unavailable error, which means nothing was done.
  • 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. Send a write again only when its error says that nothing was done, as phone_unavailable does, and observe the phone first otherwise. An action batch refused with any error but 500 internal_error ran nothing, as When nothing ran explains.
  • These rules hold only for an answer in Phonebox's error envelope. A 502 or 504 without it, a timeout or a dropped connection leaves a write's outcome unknown, as Failures without the envelope explains.
  • action_outcome_unknown is never retryable: observe first, then decide.

Common errors

Every error has the same envelope, described with every code in Errors. Each endpoint on the following pages lists its own errors. Beyond those, any request can fail with these:

ErrorWhen
401 invalid_api_key, 401 key_expired, 401 key_revokedThe key is missing, malformed or unknown, has expired, or was revoked.
403 browser_requests_not_allowedThe request carries an Origin header.
403 insufficient_scopeThe key lacks the endpoint's scope. details.required names it.
400 validation_failedA parameter, the body or the Idempotency-Key breaks a rule. details.issues lists each problem as {path, message}.
404 not_foundNo endpoint exists at this path.
405 method_not_allowedThe path exists, but not with this method. The Allow header lists its methods.
429 rate_limitedThe key made 600 requests this minute.
500 internal_errorSomething went wrong on our side. Keep the request ID. On a GET the error is retryable. On any other request it isn't, because the write may have taken effect: read the phone before you retry, as next says.
503 platform_not_configuredPhonebox itself is missing a setting. Try again later, and contact support if it lasts.

A request for one phone, under /v1/phones/{id}, can also fail with 403 phone_not_allowed when the key is limited to other phones, and 404 phone_not_found when no phone with that ID exists in the project. A phone in another project answers 404 phone_not_found too.

Requests that reach into the phone need it to be ready: observing, screenshots, actions, apps, files, device settings, reboots and live links. They can fail with these as well:

ErrorWhen
409 phone_startingThe phone is creating or starting. This error is retryable: wait with GET /v1/phones/{id}?wait=ready, as next says.
409 phone_parkingThe phone is parking. This error is retryable: wait with ?wait=parked, then start it.
409 phone_not_runningThe phone is parked or unavailable, as details.status says. next names the start request.
409 phone_failed, 409 phone_deletedThe phone can't be used again. Create a new one, with a new idempotency key if you use one.
502 provider_errorThe phone service returned an error. A read was already retried for about 15 seconds. This error is retryable, but after a write, observe the phone before you send it again.
503 phone_unavailableThe phone was briefly out of reach, and nothing was done on it. This error is retryable, and Retry-After says when to try again, when it is known.
429 rate_limitedThe phone service asked Phonebox to slow down. Retry-After and details.retry_after_seconds say how long to wait.
503 capacity_unavailableThe phone service has no capacity right now. Try again after Retry-After. When it names a phone in details.phone that has failed, it isn't retryable: create a new phone, with a new idempotency key if you use one.
502 action_outcome_unknownA write wasn't confirmed, so it may or may not have happened. This error is never retryable: observe before you do anything else.

On this page