> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phoneuse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The error envelope a failed tool call carries, and every code it can name.

A failed tool call comes back with `isError` set and the error envelope in the result's TEXT block, as JSON. It is deliberately not in `structuredContent`: a client validates any `structuredContent` it sees against the tool's output schema with no exemption for errors, so an envelope there would hard fail every standard client. Branch on `isError`, then parse the text block.

**Two things arrive with `isError` set, and only one of them is this envelope.** A call that never reached the tool, because its arguments did not match the published `inputSchema`, is refused by the protocol layer, and its text block is a SENTENCE rather than JSON:

```text theme={null}
MCP error -32602: Input validation error: Invalid arguments for tool tap: Number must be less than or equal to 1000 at x
```

So do not parse the text block unconditionally. Try JSON first and treat a parse failure as a malformed request on your side: there is no `code` to branch on, because nothing was attempted and no error in the table below occurred. This is the same refusal that catches a misspelled parameter name, and it is the most common one a new integration meets. On the REST surface the equivalent refusal DOES carry a code (`invalid_argument`, HTTP 400), so a client written against both surfaces cannot share one branch here.

## The envelope

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `name` | string | yes | - | - |
| `code` | enum | yes | one of `backend_resolution_failed`, `backend_not_implemented`, `device_unavailable`, `device_disconnected`, `lease_mismatch`, `lease_required`, `capability_unsupported`, `invalid_argument`, `backend_command_failed`, `idempotency_conflict`, `action_outcome_unknown`, `stale_frame`, `internal_error`, `unauthorized`, `permission_denied`, `run_not_found`, `frame_not_found`, `device_busy` | - |
| `message` | string | yes | - | - |
| `retryable` | boolean | yes | - | - |

The envelope also carries whatever fields the specific error adds, such as `deviceId`, `reason` or `capability`. Read the ones you branch on and ignore the rest.

`retryable` means re-issuing the SAME call unchanged is meaningful, not that you should loop. It is a property of the occurrence, not of the code: the same code can be retryable once and not the next time, so read the field rather than assuming from the code.

## Codes

The `code` field is a closed set of 44 values across MCP tools and REST. Adding one is additive; changing or removing one is a breaking change. A tool call can only answer the 18 marked "MCP tools and REST"; the rest arrive only in the REST envelope.

| Code | Where | When you get it |
| - | - | - |
| `backend_resolution_failed` | MCP tools and REST | The device's row does not name a valid backend. Correct that row: a backend is resolved per device and cannot be set anywhere else. |
| `backend_not_implemented` | MCP tools and REST | The device names a backend kind that has no adapter behind it yet. The row itself is valid; nothing can drive it today. |
| `device_unavailable` | MCP tools and REST | The device cannot be used right now. The `reason` field says which: not found, offline, unauthorized, already leased, or failed its authenticity probe. |
| `device_disconnected` | MCP tools and REST | The device dropped off its transport while the call was running. |
| `lease_mismatch` | MCP tools and REST | The lease id does not match the device's current lease. |
| `lease_required` | MCP tools and REST | An action or observe call carried no lease. Acquire the device first, then pass its `leaseId`. |
| `capability_unsupported` | MCP tools and REST | This device's backend cannot do this at all. Check its capabilities before calling. |
| `invalid_argument` | MCP tools and REST | One of your arguments failed validation, so the call was refused before it reached the device. |
| `backend_command_failed` | MCP tools and REST | The backend's own command transport failed or returned something unusable. The message names the command and stops there: whatever the transport reported is kept on our side, so do not expect to parse a device log out of this response. |
| `idempotency_conflict` | MCP tools and REST | Another action with the same `idempotencyKey` on this device is still in flight. Wait for its outcome; a different intent needs a new key. |
| `action_outcome_unknown` | MCP tools and REST | The action was dispatched but its outcome never came back, so the device may or may not have applied it. Re-observe before acting again, and use a NEW `idempotencyKey`. |
| `stale_frame` | MCP tools and REST | The action named an `observedFrame` that no longer describes the screen, so it was refused before anything physical happened. Take a new screenshot and decide again. |
| `internal_error` | MCP tools and REST | An unexpected failure. Reported by message only, with no internal detail. |
| `unauthorized` | MCP tools and REST | The request credentials are missing or invalid. |
| `permission_denied` | MCP tools and REST | The caller is authenticated but lacks the permission this call needs. |
| `run_not_found` | MCP tools and REST | No run under this id that your org can see. A run owned by another org answers the same way. |
| `frame_not_found` | MCP tools and REST | No frame under this id that you may read: it may belong to another organization, may never have finished being stored, may have passed its retention TTL, or may already have been deleted. These are deliberately indistinguishable. |
| `device_busy` | MCP tools and REST | Too many actions are already waiting on this device. Actions on one device run one at a time in the order they arrived, a bounded number may wait, and this one was refused rather than queued; nothing already waiting was dropped. Retry after `retryAfterMs`. |
| `admission_unavailable` | REST | HTTP 503 on the provisioning and billing routes when the provisioning store cannot be read right now. Retry after `Retry-After`. |
| `checkout_unavailable` | REST | HTTP 503 on the provisioning and billing routes when this deployment has no payment provider configured, the provider did not answer, the billing portal is not set up, or nothing is configured for what you asked about (a copilot allowance, a price). Nothing was changed. |
| `checkout_customer_balance` | REST | HTTP 503 from `POST /v1/checkout-sessions` when your organization's billing account holds a credit or balance that blocks checkout. Retrying does not help; contact support and we will clear it. |
| `checkout_in_progress` | REST | HTTP 409 from `POST /v1/checkout-sessions` when another checkout for your organization is open and could not be closed, usually a second tab or a second admin. Finish or abandon that one first. |
| `quote_changed` | REST | HTTP 409 from `POST /v1/checkout-sessions` when the selected model's reviewed quote no longer matches its current server-side Price. Refresh the catalog, review the new price, and submit its quoteId. No order or payable session was created. |
| `payment_in_progress` | REST | HTTP 409 from `POST /v1/checkout-sessions` when the previous checkout was already paid and that payment is still being confirmed, so it cannot be replaced. Wait for the order to appear instead of paying again. |
| `out_of_stock` | REST | HTTP 409 from `POST /v1/checkout-sessions` when this deployment refuses sales it cannot ship today, or when the model or region you chose has sold out. Nothing was charged. |
| `wrong_state` | REST | HTTP 409 from cancelling or returning an order that is not in a state where that is possible, for example returning one that was never delivered. The body's `state` names where it is. |
| `instance_not_served` | REST | HTTP 403 from `POST /v1/checkout-sessions` when your organization signs in through an instance whose purchases this deployment does not handle. Nothing you send differently will change it; contact support. |
| `reverification_required` | REST | HTTP 403 from `POST /v1/checkout-sessions` and Managed Proxy financial actions, where the deployment requires it, when the action needs you to have verified your identity recently. Creating, rotating and renewing API keys never ask. Verify again and repeat the same call; this is the one retryable 403. Carries `maxAgeMinutes`. |
| `not_found` | REST | HTTP 404 on an addressed REST route when nothing at that id is yours to act on. Unknown, another organization's and already gone are one answer, so the code says nothing about whether the id exists. |
| `receipt_not_found` | REST | HTTP 404 from the single-receipt read when no receipt under that idempotency key belongs to your organization. |
| `rate_limited` | REST | HTTP 429 when your credential is over its per-minute budget. Wait for `Retry-After` seconds; the `RateLimit-*` headers show the budget. |
| `payload_too_large` | REST | HTTP 413 when the request body is over the size limit. The message names the limit in bytes. Answered the same way on every surface. |
| `origin_not_allowed` | REST | HTTP 403 to a browser preflight from a web origin that is not on this deployment's allow-list. Call the API from your server, or from the console. |
| `api_key_from_browser_origin` | REST | HTTP 403 when an API key (`pbk_...`) arrives from a web page, which is detected by the `Origin` header. Keys are for servers; a key used in a browser is exposed to everyone who opens the page. |
| `key_predates_instance_attribution` | REST | HTTP 403 for an API key minted before its organization's sign-in instance was recorded. Rotate the key; the new one works. |
| `org_not_served_by_instance` | REST | HTTP 403 when your organization belongs to a different sign-in instance from the one that issued your credential. Sign in through the console you normally use, or mint a key there. |
| `tenant_binding_unavailable` | REST | HTTP 503 when this deployment cannot check which organization your credential belongs to right now. Retry after `Retry-After` (30 seconds). |
| `tenant_binding_unsettled` | REST | HTTP 503 on the first request for a new organization, while its binding is still being recorded. Retry after `Retry-After` (1 second). |
| `fleet_at_capacity` | REST | HTTP 409 from taking a device out of the shared pool when your organization already holds its limit. The body carries `holding`, `limit` and `retryAfterMs`. |
| `forbidden` | REST | HTTP 403 from taking a device out of the shared pool when your organization has no pay-as-you-go access to it. |
| `service_unavailable` | REST | HTTP 503 when this deployment does not serve the surface at all (copilot, direct commands). Waiting does not help, so there is no `Retry-After`. |
| `invalid_signature` | Stripe webhook | HTTP 400 from `POST /webhooks/stripe` when the payment provider's signature does not verify. Answered to the provider, never to a customer. |
| `invalid_event` | Stripe webhook | HTTP 400 from `POST /webhooks/stripe` when a correctly signed event cannot be read. Answered to the provider, never to a customer. |
| `not_settled` | Stripe webhook | HTTP 503 from `POST /webhooks/stripe` when an event cannot be applied yet, so the provider delivers it again later. Answered to the provider, never to a customer. |

## The other two surfaces

The closed set above covers MCP tool calls and the REST envelope. This service answers in three envelopes, not one, and a client that branches on the table above everywhere will break on the other two. Each surface always answers in its own envelope.

| Surface | Envelope | Codes |
| - | - | - |
| `POST /mcp` tool calls | `{ name, code, message, retryable }` in the result's text block | the 18 marked "MCP tools and REST" above |
| REST, `/v1/*` | `{ error: { code, ... } }`, HTTP status carries the outcome | all 44 above, plus `reason` on a `401` |
| Device surface, `/vrx/{runToken}/v1/*` | `{ ok: false, error: { code, message } }` | SCREAMING\_SNAKE, the closed set below |

The REST envelope carries `retryable` when the refusal knows the answer: it is a property of the occurrence rather than of the code, so read the field instead of inferring it from the code. The HTTP status still carries the outcome, and a `401` additionally carries a `reason` naming which credential problem it was.

### Device surface codes

The device surface speaks the `visualremote/1` dialect, so it has its OWN closed set of 10 codes. The two sets are deliberately separate: the dialect stops at the device channel rather than spreading into the rest of this API. Branch on these when you are writing a worker.

| Code |
| - |
| `DEVICE_NOT_FOUND` |
| `INVALID_ACTION` |
| `FRAME_VIEWPORT_MISMATCH` |
| `COORDINATE_OUT_OF_REACH` |
| `ACTION_UNAVAILABLE` |
| `FENCING_TOKEN_STALE` |
| `STALE_FRAME` |
| `HARDWARE_OFFLINE` |
| `FRAME_UNAVAILABLE` |
| `ACTION_OUTCOME_UNKNOWN` |

One of them is worth calling out. `DEVICE_NOT_FOUND` covers an unknown, expired, terminal or wrong-device run token, all answering identically so that a token holder cannot probe which it was. It is also what you get if you point an ORG API KEY at this surface, and in that case nothing is missing: this is the worker channel, its credential is the run token in the url, and no `Authorization` header is read at all. Drive devices yourself over `/mcp` instead.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.