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

# API overview

> Two HTTP surfaces, and where the boundary between them runs.

The control plane exposes two HTTP surfaces. They differ in who calls them and
in how a caller proves it is allowed to. Reading the boundary correctly matters
more than reading any single endpoint, so it comes first.

## The two surfaces

| Surface | Paths | Credential | Called by |
| - | - | - | - |
| Control | `/healthz`, `/.well-known/oauth-protected-resource`, `/v1/api-keys`, `/v1/runs` | Bearer token in the `Authorization` header | Your backend, and operators |
| Device | `/vrx/{runToken}/v1/...` | The run token in the URL path | A worker executing one run |

The control surface is the one you call to manage credentials, start runs, and
check health. The device surface is what a worker uses to observe and act on the
one device its run is bound to.

## Driving a device

Most callers never touch either surface directly for device control. An agent
drives a device over MCP, at `POST /mcp`, and that endpoint speaks JSON-RPC
rather than REST. Its shape is documented in the
[tool reference](/mcp/reference), not here, because the unit of the MCP surface
is a tool rather than an endpoint.

If you are looking for something like `POST /v1/devices/{id}/tap`, it does not
exist. Tapping is a tool call. Start at the [Quickstart](/quickstart).

## Authentication on the control surface

Two credential channels resolve to the same authorization context, so nothing
downstream needs to know which one you used.

* **OAuth 2.1** for interactive clients. The control plane is a resource server
  only. Discovery follows RFC 9728 at
  `/.well-known/oauth-protected-resource`, which is also served under the
  `/mcp` suffix because clients try both.
* **API keys** for headless callers. A key is scoped to one org, and may be
  narrowed further to a subset of that org's devices.

Scopes are enforced when tools are registered rather than when they are called,
so a tool your credential cannot use is not merely refused, it is absent. See
[Authentication](/mcp/auth).

## Calling from a browser

### Browser access

Browsers are refused by default. Cross origin requests from a web page only work
if that page's origin has been added to the deployment's allow list, and the
allow list is exact: there is no wildcard, and no subdomain matching. If you want
to call this API from a web application, ask for your origin to be added.

Three things about that grant are worth stating plainly, because each one is a
decision rather than an accident.

* **API keys are refused from browsers.** On a deployment that grants browser
  access at all, a request carrying both an `Origin` header and a `pbk_` key is
  answered `403 api_key_from_browser_origin`, and that holds for every origin,
  allow-listed or not. The refusal ships with the grant, so a deployment with no
  allow list configured does not apply it: there, such a request is
  authenticated in the ordinary way, because no browser can reach the API cross
  origin in the first place. An API key is a long lived, org wide credential, so
  a page holding one has effectively published it. Authenticate a browser with
  an OAuth session token instead. This applies to the control surface; if a
  server side integration of yours forwards a browser's `Origin` header
  upstream, strip it.
* **Only the control surface is granted.** `POST /mcp` and the device surface at
  `/vrx/{runToken}/v1/...` refuse every request carrying an `Origin` header, and
  neither is part of the grant, so a page cannot call them with `fetch` and can
  never read a response from them. Note the exact shape of that guarantee: a
  browser omits `Origin` on a plain subresource load (an `img` tag, say), so a
  page that has somehow obtained a run token could still cause a `GET` on the
  device surface to execute, blind. Treat a run token as the device credential
  it is, and keep it out of pages you do not control.
* **Credentials are sent in the header, never as cookies.** The grant does not
  enable `Access-Control-Allow-Credentials`, so send your bearer token in the
  `Authorization` header as any other client would.

Rate limit headers and `WWW-Authenticate` are explicitly exposed to browser
callers, so a page can read the budget it is being held to and can tell an
expired token from a refused one. See [Rate limits](/api/rate-limits).

## The device surface and its run token

The device surface exists so a worker can observe and act without holding an org
credential. The token in the path **is** the credential: the surface reads no
`Authorization` header, and sending one changes nothing.

That token carries the org, the device, the lease and the run it belongs to, and
it expires. It is bound to exactly one device, so it cannot be used to reach
anything else in the fleet.

<Warning>
  A run token is a credential. Treat it like one. Never paste it into an issue,
  a support thread, or a log you share, and never commit one. Every example on
  this site writes `{runToken}` as a placeholder for that reason.
</Warning>

Requesting a frame on this surface is metered before the pixels are returned. If
the metering write fails, the frame is withheld and the request fails. That
ordering is deliberate: it means no frame is ever served without being counted.

See [The contract in full](/agent/worker#the-contract-in-full) for everything a
worker may call on this surface.

## Rate limits

In hosted mode both surfaces pass a limiter before anything else looks at the
request, and which budget applies is decided by whether that request carries an
`Authorization` header. Control surface calls do, so they are in the
credentialed class. A worker on the device surface sends no such header, because
its credential is in the URL path, so it is in the bare class. Local mode has no
limiter at all.

Every limited response carries its own budget in `RateLimit-*` headers, and
those are authoritative over any number written down. See
[Rate limits](/api/rate-limits).

## Health

`GET /healthz` is unauthenticated and reports whether the service is up. In
hosted mode it also reports whether the billing pipeline is healthy. It is the
one path the limiter never applies to.


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