Conventions
The conventions every API request follows, from authentication and idempotency to pagination, rate limits and money, and the list of endpoints.
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
| Operation | Method and path | Scope |
|---|---|---|
| Create a phone | POST /v1/phones | phones:create |
| List phones | GET /v1/phones | phones:read |
| Get a phone | GET /v1/phones/{id} | phones:read |
| Update a phone | PATCH /v1/phones/{id} | phones:control |
| Delete a phone | DELETE /v1/phones/{id} | phones:delete |
| Start a phone | POST /v1/phones/{id}/start | phones:control |
| Park a phone | POST /v1/phones/{id}/park | phones:control |
| Keep a phone awake | POST /v1/phones/{id}/heartbeat | phones:control |
| List a phone's sessions | GET /v1/phones/{id}/sessions | phones:read |
| Observe the screen | GET /v1/phones/{id}/observe | phones:read |
| Take a screenshot | GET /v1/phones/{id}/screenshot | phones:read |
| Run actions | POST /v1/phones/{id}/actions | phones:control |
| Search the app library | GET /v1/apps/library | phones:read |
| List apps | GET /v1/phones/{id}/apps | phones:read |
| Install an app | POST /v1/phones/{id}/apps | phones:control |
| List app installs | GET /v1/phones/{id}/apps/installs | phones:read |
| Uninstall an app | DELETE /v1/phones/{id}/apps/{package} | phones:control |
| Create an upload | POST /v1/apps/uploads | phones:control |
| List uploads | GET /v1/apps/uploads | phones:read |
| Get an upload | GET /v1/apps/uploads/{id} | phones:read |
| Complete an upload | POST /v1/apps/uploads/{id}/complete | phones:control |
| Delete an upload | DELETE /v1/apps/uploads/{id} | phones:control |
| List files | GET /v1/phones/{id}/files | phones:read |
| Upload a file | PUT /v1/phones/{id}/files | phones:control |
| Download a file | GET /v1/phones/{id}/files/content | phones:read |
| Delete a file | DELETE /v1/phones/{id}/files | phones:control |
| Get device info | GET /v1/phones/{id}/device | phones:read |
| Read the clipboard | GET /v1/phones/{id}/clipboard | phones:control |
| Set the clipboard | PUT /v1/phones/{id}/clipboard | phones:control |
| Read the location | GET /v1/phones/{id}/location | phones:read |
| Set the location | PUT /v1/phones/{id}/location | phones:control |
| Reset the location | DELETE /v1/phones/{id}/location | phones:control |
| Read the locale | GET /v1/phones/{id}/locale | phones:read |
| Set the locale | PUT /v1/phones/{id}/locale | phones:control |
| Read the timezone | GET /v1/phones/{id}/timezone | phones:read |
| Set the timezone | PUT /v1/phones/{id}/timezone | phones:control |
| Reboot | POST /v1/phones/{id}/reboot | phones:control |
| Reset | POST /v1/phones/{id}/reset | phones:delete |
| Start a recording | POST /v1/phones/{id}/recordings | phones:control |
| List recordings | GET /v1/phones/{id}/recordings | phones:read |
| Stop a recording | POST /v1/phones/{id}/recordings/{rid}/stop | phones:control |
| Download a recording | GET /v1/phones/{id}/recordings/{rid}/video | phones:read |
| Delete a recording | DELETE /v1/phones/{id}/recordings/{rid} | phones:control |
| Create a live view link | POST /v1/phones/{id}/live | phones:control |
| Get the account | GET /v1/account | phones:read |
| Create a checkout link | POST /v1/credits/checkout | phones:control |
| Start terminal setup | POST /v1/setup | No key |
| Poll terminal setup | POST /v1/setup/token | No 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 with400 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_failedinstead 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.waitisn'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 indetails.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-Keyit 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
nextof a write'sinternal_errorsays. - 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.
| Parameter | Type | Default | Rules |
|---|---|---|---|
limit | integer | 50 | From 1 to 100 items. |
cursor | string | None | The 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
| Limit | Applies to |
|---|---|
| 600 requests a minute | Each API key, across all its requests. Phonebox counts it on each of its servers, so treat it as approximate. |
| 60 lifecycle requests a minute | Each project: creates, starts and renewals, parks and deletes, repeats included. A replayed create doesn't count. This limit is exact. |
| 1 reboot every 10 minutes | Each phone. |
| 10 checkout links an hour | Each project. This limit is exact. |
| 10 setup requests a minute | Each IP address. Approximate, like the key limit. |
| 60 setup polls a minute | Each setup request. Approximate, like the key limit. |
| 1 reset a minute | Each phone. A reset also has to wait until the phone is ready again. |
| 20 recordings | Each 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_timeoutandmax_duration. Inside actions,ms,duration_msandtimeout_msare 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_unavailableerror, which means nothing was done. - When an error's
retryableistrue, the same request may succeed later. Wait forRetry-Afterwhen the response has one, or a few seconds when it doesn't. For a write,retryabledoesn't mean that the first attempt did nothing. Send a write again only when its error says that nothing was done, asphone_unavailabledoes, and observe the phone first otherwise. An action batch refused with any error but500 internal_errorran nothing, as When nothing ran explains. - These rules hold only for an answer in Phonebox's error envelope. A
502or504without it, a timeout or a dropped connection leaves a write's outcome unknown, as Failures without the envelope explains. action_outcome_unknownis 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:
| Error | When |
|---|---|
401 invalid_api_key, 401 key_expired, 401 key_revoked | The key is missing, malformed or unknown, has expired, or was revoked. |
403 browser_requests_not_allowed | The request carries an Origin header. |
403 insufficient_scope | The key lacks the endpoint's scope. details.required names it. |
400 validation_failed | A parameter, the body or the Idempotency-Key breaks a rule. details.issues lists each problem as {path, message}. |
404 not_found | No endpoint exists at this path. |
405 method_not_allowed | The path exists, but not with this method. The Allow header lists its methods. |
429 rate_limited | The key made 600 requests this minute. |
500 internal_error | Something 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_configured | Phonebox 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:
| Error | When |
|---|---|
409 phone_starting | The phone is creating or starting. This error is retryable: wait with GET /v1/phones/{id}?wait=ready, as next says. |
409 phone_parking | The phone is parking. This error is retryable: wait with ?wait=parked, then start it. |
409 phone_not_running | The phone is parked or unavailable, as details.status says. next names the start request. |
409 phone_failed, 409 phone_deleted | The phone can't be used again. Create a new one, with a new idempotency key if you use one. |
502 provider_error | The 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_unavailable | The 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_limited | The phone service asked Phonebox to slow down. Retry-After and details.retry_after_seconds say how long to wait. |
503 capacity_unavailable | The 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_unknown | A write wasn't confirmed, so it may or may not have happened. This error is never retryable: observe before you do anything else. |