Skip to main content
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

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

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.

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.

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

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.