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

# Open a live view of a device

> Ask how to show this device's screen live, and get everything the player needs in one answer. Every device answers with the same shape; `transport` says which entry of `connect` to use.

`edge-webrtc`: a robot-driven phone whose arm can publish video, on a deployment with a media server. `connect["edge-webrtc"]` holds the media server `url`, the `room`, and a `token` that can only receive, valid for joining until `expiresAt`: 10 minutes, or sooner when your organisation's lease on the device ends sooner. Ask again before it lapses. The room belongs to the current lease: the next holder of the device gets a different room, so keep the lease if you want to keep the picture. The arm starts publishing when you first call this; calling again under the same lease keeps the running stream and returns the same `streamId`. An arm that is reachable but has nothing to publish (its screen feed is down, or it does not support video) answers `snapshot` instead. An arm that is reachable but not ready to act (for example awaiting calibration) can still be watched live when its screen feed is up; it has no still-image fallback, so otherwise it answers 409 like every other device route.

`provider-rtc`: a cloud phone. `connect["provider-rtc"]` holds the same five fields `POST /v1/devices/{deviceId}/live-session` returns, for the provider's video SDK.

`snapshot`: no live video for this device here. Poll `fallback.screenshot` no faster than `fallback.maxFps`.

Controls never travel over the video channel. Send gestures to `controls.actions` as usual; `controls.via` is `sdk` only for a cloud phone with no agent run on it, whose player sends a person's input through the provider. While a run drives a cloud phone, `controls.via` is `api`: send the person's gestures to `controls.actions`, so the run is told somebody else acted before its next action. `via` is decided when you open the view; `GET /v1/devices/{deviceId}/driving` reports a `run` exactly while it would say `api`, so read that to notice a run starting or ending while the view is open. When this service cannot tell whether a run is on the device, it says `api`.

PERSON ONLY and YOU NEED THE SEAT, exactly as for a live session: a signed-in person with `devices:act` whose organisation holds a lease on the device (theirs, a colleague's, or a running agent's). Opening a view never takes the device or interrupts an agent. Tokens are credentials; do not store them.

Hosted deployments only. A local checkout does not mount this route, so calling it there is a 404.



## OpenAPI

````yaml /api/openapi.json post /v1/devices/{deviceId}/stream
openapi: 3.1.0
info:
  title: Phonebase HTTP API
  version: 0.1.0
  summary: >-
    Every HTTP operation the control plane mounts, generated from the app's own
    route table.
  description: >-
    The control plane exposes two HTTP surfaces, and reading the boundary
    between them matters more than reading any single endpoint.


    The CONTROL surface is what your backend and your operators call. It takes a
    bearer token in the `Authorization` header, and two credential channels
    resolve to the same authorization context: an OAuth 2.1 access token for
    interactive clients, or an org scoped API key for headless ones.


    The DEVICE surface, under `/vrx/{runToken}/v1`, is what a worker calls while
    executing one run. Its credential is the run token in the url path, and it
    reads no `Authorization` header at all.


    Device control itself is not REST. An agent taps, swipes and reads the
    screen over the Model Context Protocol at `POST /mcp`, one tool call at a
    time. There is no `POST /v1/devices/{id}/tap`, and if you are looking for
    one, start with the tool reference instead.


    Operations marked `"x-phonebase-modes": ["hosted"]` exist only on a hosted
    deployment; a local checkout does not mount them.
servers:
  - url: '{origin}'
    description: Your control plane origin.
    variables:
      origin:
        default: https://mcp.phonebase.co
        enum:
          - https://mcp.phonebase.co
          - http://127.0.0.1:8788
        description: >-
          The hosted deployment serves the public origin its metadata document
          advertises, and that is the default here. Select the loopback entry
          instead when you are running a local checkout, which serves the port
          `PORT` names.
security: []
tags:
  - name: Apps
    description: Save immutable application builds and manage explicit device operations.
  - name: Managed Proxy
    description: >-
      Organization-owned fixed egress resources, independent one-time orders and
      verified network relationships.
  - name: Health
    description: Liveness, unauthenticated.
  - name: Gateway enrollment
    description: >-
      How a new gateway computer trades a one-time enrollment code for its
      tunnel credential. Unauthenticated: the code is the credential.
  - name: Gateway releases
    description: >-
      What a gateway computer installs: the current edge release of a channel.
      Called with the gateway's own credential.
  - name: MCP
    description: The JSON-RPC endpoint an agent drives devices through.
  - name: Copilot
    description: >-
      Drive a device by chatting. A signed in person's own conversations,
      streamed. Hosted deployments only.
  - name: Discovery
    description: Where to authenticate. Served on hosted deployments only.
  - name: Runs
    description: Start, watch and steer a run.
  - name: API keys
    description: Mint and revoke headless credentials. Hosted deployments only.
  - name: Device surface
    description: What a worker calls with its run token to observe and act.
  - name: Usage
    description: Read your org's own receipts and consumption. Hosted deployments only.
  - name: Devices
    description: Read the fleet your credential can see. Hosted deployments only.
  - name: Notifications
    description: >-
      What a person needs to know when they are not watching, and which emails
      they receive. Hosted deployments only.
  - name: Webhooks
    description: >-
      Subscribe your own system to what happens here, and read what was
      delivered. Hosted deployments only.
  - name: Provisioning
    description: >-
      Whether an organisation is provisioned, and how it becomes so. Hosted
      deployments only, and both operations answer a declared 503 on a
      deployment that cannot provision yet.
paths:
  /v1/devices/{deviceId}/stream:
    post:
      tags:
        - Devices
      summary: Open a live view of a device
      description: >-
        Ask how to show this device's screen live, and get everything the player
        needs in one answer. Every device answers with the same shape;
        `transport` says which entry of `connect` to use.


        `edge-webrtc`: a robot-driven phone whose arm can publish video, on a
        deployment with a media server. `connect["edge-webrtc"]` holds the media
        server `url`, the `room`, and a `token` that can only receive, valid for
        joining until `expiresAt`: 10 minutes, or sooner when your
        organisation's lease on the device ends sooner. Ask again before it
        lapses. The room belongs to the current lease: the next holder of the
        device gets a different room, so keep the lease if you want to keep the
        picture. The arm starts publishing when you first call this; calling
        again under the same lease keeps the running stream and returns the same
        `streamId`. An arm that is reachable but has nothing to publish (its
        screen feed is down, or it does not support video) answers `snapshot`
        instead. An arm that is reachable but not ready to act (for example
        awaiting calibration) can still be watched live when its screen feed is
        up; it has no still-image fallback, so otherwise it answers 409 like
        every other device route.


        `provider-rtc`: a cloud phone. `connect["provider-rtc"]` holds the same
        five fields `POST /v1/devices/{deviceId}/live-session` returns, for the
        provider's video SDK.


        `snapshot`: no live video for this device here. Poll
        `fallback.screenshot` no faster than `fallback.maxFps`.


        Controls never travel over the video channel. Send gestures to
        `controls.actions` as usual; `controls.via` is `sdk` only for a cloud
        phone with no agent run on it, whose player sends a person's input
        through the provider. While a run drives a cloud phone, `controls.via`
        is `api`: send the person's gestures to `controls.actions`, so the run
        is told somebody else acted before its next action. `via` is decided
        when you open the view; `GET /v1/devices/{deviceId}/driving` reports a
        `run` exactly while it would say `api`, so read that to notice a run
        starting or ending while the view is open. When this service cannot tell
        whether a run is on the device, it says `api`.


        PERSON ONLY and YOU NEED THE SEAT, exactly as for a live session: a
        signed-in person with `devices:act` whose organisation holds a lease on
        the device (theirs, a colleague's, or a running agent's). Opening a view
        never takes the device or interrupts an agent. Tokens are credentials;
        do not store them.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: postV1DevicesByDeviceIdStream
      parameters:
        - name: deviceId
          in: path
          required: true
          description: The `deviceId` from a listing.
          schema:
            type: string
      requestBody:
        description: >-
          Optional. `tracks` and `layer` are accepted and not read yet: every
          view starts with the screen track on the high layer.
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                tracks:
                  type: array
                  items:
                    type: string
                    enum:
                      - screen
                      - overhead
                layer:
                  type: string
                  enum:
                    - auto
                    - low
                    - medium
                    - high
      responses:
        '200':
          description: How to show the device.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceStream'
        '401':
          description: No credential, or one that was refused.
          headers:
            WWW-Authenticate:
              description: >-
                A bearer challenge. On a hosted deployment it names the metadata
                document to discover the authorization server from, and it adds
                `error="invalid_token"` when a credential was presented and
                rejected.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnauthorizedError'
        '403':
          description: >-
            A live view needs a signed-in person with `devices:act`; an API key
            is refused.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such device that this credential can see.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            The device is offline or its arm could not be reached, or your
            organisation holds no lease on it (take it first).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: >-
            The request body exceeds the server's limit (1 MiB). Refused before
            the body is parsed, so nothing was partially applied. Retrying the
            same body will fail the same way: send less. Per-field limits are
            tighter than this and are documented on the fields themselves; this
            is the backstop underneath them. The shape is the same on ordinary
            JSON surfaces, /vrx included. The explicitly configured APK content
            stream documents its own byte ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayloadTooLargeError'
        '429':
          description: >-
            Over the request budget for this caller. Hosted deployments only.
            Which budget applied depends on whether this request carried an
            `Authorization` header, not on which endpoint it was aimed at. Wait
            `Retry-After` seconds and retry the same request: the refusal
            happens before any work, so nothing was partially applied. See
            /api/rate-limits.
          headers:
            Retry-After:
              description: >-
                Whole seconds until one request is available again. Computed
                from the caller's own remaining allowance, not a fixed constant.
              schema:
                type: string
            RateLimit-Limit:
              description: >-
                The budget in effect for this caller class, in requests per
                minute.
              schema:
                type: string
            RateLimit-Remaining:
              description: >-
                Whole requests that were still available when this request was
                admitted. Zero on a 429.
              schema:
                type: string
            RateLimit-Reset:
              description: >-
                Seconds until the allowance is back to full. Longer than
                `Retry-After`, which is the wait for a single request.
              schema:
                type: string
            RateLimit-Policy:
              description: The budget and its window, as `<limit>;w=60`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitedError'
        '503':
          description: >-
            The authentication backend could not be reached. The service fails
            closed: an infrastructure fault is refused, never waved through.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
components:
  schemas:
    DeviceStream:
      type: object
      properties:
        streamId:
          type: string
          description: An opaque id for this view.
        transport:
          type: string
          enum:
            - edge-webrtc
            - provider-rtc
            - snapshot
          description: Which player to use, and which entry of `connect` it reads.
        expiresAt:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            When the viewing token stops admitting a new join, or null when
            there is none or it is not known. Ask again before it.
        tracks:
          type: array
          description: >-
            The video tracks on offer. Sizes and rates are nominal; the player
            reads the real ones from the stream.
          items:
            type: object
            properties:
              role:
                type: string
                enum:
                  - screen
                  - overhead
                description: '`screen` is the phone''s screen.'
              width:
                type: integer
                description: Nominal width in pixels; 0 when not known yet.
              height:
                type: integer
                description: Nominal height in pixels; 0 when not known yet.
              fps:
                type: number
                description: Nominal frames per second.
              layers:
                type: array
                items:
                  type: string
                  enum:
                    - low
                    - medium
                    - high
                description: >-
                  Quality layers on offer, as the source reports publishing them
                  (a robot arm's live stream: low, medium and high once it is
                  publishing; `high` alone before that and for a cloud phone).
            required:
              - role
              - width
              - height
              - fps
              - layers
            additionalProperties: false
        connect:
          type: object
          description: >-
            Connection details keyed by transport; exactly the entry named by
            `transport` is present, and none for `snapshot`. `edge-webrtc` is
            `{url, token, room}` with a receive-only token; `provider-rtc` is
            `{token, baseUrl, padCode, userId, resolution}`. Tokens are
            credentials.
          properties:
            edge-webrtc:
              type: object
              properties:
                url:
                  type: string
                  description: The media server to connect to.
                token:
                  type: string
                  description: A receive-only token for `room`.
                room:
                  type: string
                  description: The device's room.
              required:
                - url
                - token
                - room
              additionalProperties: false
            provider-rtc:
              type: object
              properties:
                token:
                  type: string
                  description: A short-lived provider SDK credential.
                baseUrl:
                  type: string
                  description: The endpoint the SDK connects to.
                padCode:
                  type: string
                  description: The device's provider instance code.
                userId:
                  type: string
                  description: A stable per-person id for the SDK.
                resolution:
                  type: integer
                  description: The SDK resolution profile.
              required:
                - token
                - baseUrl
                - padCode
                - userId
                - resolution
              additionalProperties: false
          additionalProperties: false
        clock:
          type: object
          properties:
            serverTime:
              type: string
              format: date-time
              description: The server's clock when this answer was made.
            frameTiming:
              type: string
              enum:
                - sync_points
                - edge_clock
                - none
              description: >-
                How the player can tell when a frame was captured: `sync_points`
                from timing messages in the room, `edge_clock` from the
                screenshot's `X-Phonebase-Captured-At`, `none` when it cannot.
          required:
            - serverTime
            - frameTiming
          additionalProperties: false
        viewer:
          type: object
          properties:
            role:
              type: string
              enum:
                - customer
              description: Who this view is for.
            defaultLayer:
              type: string
              enum:
                - low
                - medium
                - high
              description: The layer to start on.
            priority:
              type: string
              enum:
                - customer
              description: Scheduling priority of this viewer.
          required:
            - role
            - defaultLayer
            - priority
          additionalProperties: false
        controls:
          type: object
          properties:
            via:
              type: string
              enum:
                - api
                - sdk
              description: >-
                Where a person's gestures go: this API's actions route, or the
                provider's SDK. `sdk` only for a cloud phone with no agent run
                on it; while a run drives the device it is `api`.
            actions:
              type: string
              description: The actions route for this device.
            events:
              type:
                - string
                - 'null'
              description: >-
                The event stream for this device's view, or null while there is
                none.
          required:
            - via
            - actions
            - events
          additionalProperties: false
        capabilities:
          type: object
          properties:
            tracks:
              type: array
              items:
                type: string
                enum:
                  - screen
                  - overhead
              description: Tracks this device can show.
            inputs:
              type: object
              properties:
                tap:
                  type: boolean
                  description: Whether `tap` is accepted.
                swipe:
                  type: boolean
                  description: Whether `swipe` is accepted.
                long_press:
                  type: boolean
                  description: Whether `long_press` is accepted.
                press_key:
                  type: boolean
                  description: Whether `press_key` is accepted.
                type_text:
                  type: boolean
                  description: Whether `type_text` is accepted.
                hover:
                  type: boolean
                  description: Whether hovering the pen is accepted. Always false today.
                hover_end:
                  type: boolean
                  description: Whether ending a hover is accepted. Always false today.
              required:
                - tap
                - swipe
                - long_press
                - press_key
                - type_text
                - hover
                - hover_end
              additionalProperties: false
              description: >-
                Which gestures the actions route accepts for this device, by its
                `action` names.
            keys:
              type: array
              items:
                type: string
                enum:
                  - home
                  - back
                  - enter
                  - delete
                  - app_switch
                  - power
                  - volume_up
                  - volume_down
                description: A pressable key.
              description: Keys `press_key` accepts; empty when none.
            multiTouch:
              type: boolean
              description: Always false today.
            rotate:
              type: boolean
              description: Always false today.
            hoverPremove:
              type: boolean
              description: Always false today.
            recording:
              type: boolean
              description: Always false today.
            unavailable:
              type: object
              additionalProperties:
                type: string
              description: >-
                Input name to a sentence explaining why it is unavailable. Empty
                today.
          required:
            - tracks
            - inputs
            - keys
            - multiTouch
            - rotate
            - hoverPremove
            - recording
            - unavailable
          additionalProperties: false
          description: What a player may offer on this device.
        fallback:
          type: object
          properties:
            transport:
              type: string
              enum:
                - snapshot
              description: The fallback is always still images.
            screenshot:
              type: string
              description: The screenshot route to poll.
            maxFps:
              type: number
              description: Poll no faster than this many times per second.
          required:
            - transport
            - screenshot
            - maxFps
          additionalProperties: false
      required:
        - streamId
        - transport
        - expiresAt
        - tracks
        - connect
        - clock
        - viewer
        - controls
        - capabilities
        - fallback
      additionalProperties: false
      description: How to show one device live.
    UnauthorizedError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              const: unauthorized
              description: Always `unauthorized` on this response.
            reason:
              type: string
              enum:
                - missing_credentials
                - malformed_credentials
                - invalid_token
                - token_expired
                - wrong_audience
                - missing_org
                - missing_org_role
                - unknown_key
                - revoked_key
                - expired_key
              description: The closed set of ways authentication can fail.
            hint:
              type: string
              description: >-
                A next step in prose, on the reasons where there is one to give:
                `revoked_key`, `expired_key`, `missing_org_role`, and
                `invalid_token` when the account is not a member of the named
                organization. Absent otherwise, so branch on `reason` and show
                this if it is there.
          required:
            - code
            - reason
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      description: >-
        A refused credential. `unknown_key` covers both a keyId nobody has and a
        secret that does not match, because telling those apart would enumerate
        keys for you. Revocation and expiry are distinguishable, since you owned
        that key.
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - 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
                - admission_unavailable
                - checkout_unavailable
                - checkout_customer_balance
                - checkout_in_progress
                - quote_changed
                - payment_in_progress
                - out_of_stock
                - wrong_state
                - instance_not_served
                - reverification_required
                - not_found
                - receipt_not_found
                - rate_limited
                - payload_too_large
                - origin_not_allowed
                - api_key_from_browser_origin
                - key_predates_instance_attribution
                - org_not_served_by_instance
                - tenant_binding_unavailable
                - tenant_binding_unsettled
                - fleet_at_capacity
                - forbidden
                - service_unavailable
                - invalid_signature
                - invalid_event
                - not_settled
              description: >-
                The closed set of codes this service answers with on purpose, on
                MCP tools and REST alike (D152). What each one means and when
                you get it is on the errors page.
            message:
              type: string
              description: A human readable summary. Do not branch on it, branch on `code`.
            retryable:
              type: boolean
              description: >-
                Whether re-issuing the same call unchanged is meaningful. It is
                a property of the occurrence, not of the code, so read the field
                rather than inferring it.
          required:
            - code
            - message
          additionalProperties: true
          description: >-
            Specific errors add their own fields, such as `deviceId`, `reason`
            or `capability`. Read the ones you branch on and ignore the rest.
      required:
        - error
      additionalProperties: false
      description: >-
        The control surface error envelope. Every non 2xx JSON body on this
        surface has this shape.
    PayloadTooLargeError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              const: payload_too_large
              description: Always `payload_too_large` on this response.
            message:
              type: string
              description: A human readable summary naming the limit in bytes.
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      description: >-
        Returned when the request body exceeds the server limit. Identical on
        every surface, including /vrx.
    RateLimitedError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              const: rate_limited
              description: Always `rate_limited` on this response.
            message:
              type: string
              description: >-
                A human readable summary. The numbers are in the headers, not in
                here.
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      description: >-
        Over the request budget. Raised by the rate limiter before any handler
        runs, which is why its code is not one of the action codes: nothing was
        attempted, so no action failed.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        The control surface credential. Send `Authorization: Bearer <token>`.


        Two kinds of token are accepted and they are told apart by shape, not by
        a separate header. A token beginning `pbk_` is an org scoped API key,
        whose public half and secret half are generated together and of which
        only a hash of the secret is ever stored; anything else is treated as an
        OAuth 2.1 access token and verified against the authorization server's
        keys.


        Both resolve to the same context: an org, a principal and a set of
        scopes. Nothing downstream branches on which channel you used, with one
        deliberate exception, key management, which requires a signed-in person
        so that a key can never mint another key.


        Scopes are enforced when MCP tools are REGISTERED rather than when they
        are called, so a tool your credential cannot use is absent from
        `tools/list` rather than refused mid gesture.

````

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