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

# Run one tool the person named

> Run ONE tool directly, as yourself, with no model involved and no tokens spent. This is what a `/screenshot` or a `/press_key home` typed in the console's composer becomes.

It reaches the SAME tool surface a copilot turn drives, under the same permissions and the same approval table, so a command cannot do what the conversation would refuse. Two things differ from a turn, and both follow from the person having typed the tool themselves. It works on a deployment with NO copilot configured, because pressing Home should not need a model. And the receipt is not labelled `via copilot`: you did this, not the copilot on your behalf.

WHAT IS DIRECT. Every read, plus `open_app`, `press_key`, `release_device`, `list_runs`, `cancel_run` and `resume_run`: the tools whose arguments a person can write out in full. `tap`, `swipe`, `type_text` and `long_press` are NOT, because the useful form of "tap the login button" is a sentence and the coordinates are the copilot's job; nor is `start_run`, which takes a goal and a budget. Those answer 400 and say to ask in the conversation instead. A tool your session has no scope for is a different answer: 403, naming the scope, because the remedy is an administrator rather than a rephrasing.

WHICH PHONE. `context` and `mentions` work exactly as they do on `POST /v1/copilot`, so a command typed on a device's page needs no `deviceId`. A tool that needs one, with nothing in the input and nothing the context could resolve, is 400.

IN A CONVERSATION. Send `threadId` and the result is appended to that thread as a tool part, which is what makes a command followed by a sentence work: the next turn's copilot sees what the command did instead of a gap. Without it the command still runs and leaves no trace in any conversation.

IF IT ASKS FIRST. `install_app` and `start_run` raise a card in a command exactly as they do in the chat: the answer is 202 with an `approval` descriptor and an `approvalId`, and nothing has run. Show the card, then send the SAME `tool` and `input` again with that `approvalId`. The question is stored on the thread, so it can be answered minutes later and on any machine, and a gated command therefore needs a `threadId` to put its card in. The tool and arguments are checked against what was asked: a yes to one command cannot be spent on another.

Needs a signed in person, as every copilot route does.

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/copilot/commands
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/copilot/commands:
    post:
      tags:
        - Copilot
      summary: Run one tool the person named
      description: >-
        Run ONE tool directly, as yourself, with no model involved and no tokens
        spent. This is what a `/screenshot` or a `/press_key home` typed in the
        console's composer becomes.


        It reaches the SAME tool surface a copilot turn drives, under the same
        permissions and the same approval table, so a command cannot do what the
        conversation would refuse. Two things differ from a turn, and both
        follow from the person having typed the tool themselves. It works on a
        deployment with NO copilot configured, because pressing Home should not
        need a model. And the receipt is not labelled `via copilot`: you did
        this, not the copilot on your behalf.


        WHAT IS DIRECT. Every read, plus `open_app`, `press_key`,
        `release_device`, `list_runs`, `cancel_run` and `resume_run`: the tools
        whose arguments a person can write out in full. `tap`, `swipe`,
        `type_text` and `long_press` are NOT, because the useful form of "tap
        the login button" is a sentence and the coordinates are the copilot's
        job; nor is `start_run`, which takes a goal and a budget. Those answer
        400 and say to ask in the conversation instead. A tool your session has
        no scope for is a different answer: 403, naming the scope, because the
        remedy is an administrator rather than a rephrasing.


        WHICH PHONE. `context` and `mentions` work exactly as they do on `POST
        /v1/copilot`, so a command typed on a device's page needs no `deviceId`.
        A tool that needs one, with nothing in the input and nothing the context
        could resolve, is 400.


        IN A CONVERSATION. Send `threadId` and the result is appended to that
        thread as a tool part, which is what makes a command followed by a
        sentence work: the next turn's copilot sees what the command did instead
        of a gap. Without it the command still runs and leaves no trace in any
        conversation.


        IF IT ASKS FIRST. `install_app` and `start_run` raise a card in a
        command exactly as they do in the chat: the answer is 202 with an
        `approval` descriptor and an `approvalId`, and nothing has run. Show the
        card, then send the SAME `tool` and `input` again with that
        `approvalId`. The question is stored on the thread, so it can be
        answered minutes later and on any machine, and a gated command therefore
        needs a `threadId` to put its card in. The tool and arguments are
        checked against what was asked: a yes to one command cannot be spent on
        another.


        Needs a signed in person, as every copilot route does.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: postV1CopilotCommands
      requestBody:
        description: The command.
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tool:
                  type: string
                  description: The tool name, as the MCP reference spells it.
                input:
                  type: object
                  additionalProperties: true
                  description: The tool's arguments, in its own published input schema.
                context:
                  type: object
                  properties:
                    route:
                      type: string
                      description: >-
                        The console route being viewed, e.g. `/devices/pixel-1`.
                        Orientation for the model; never parsed for ids.
                      maxLength: 200
                    device:
                      type: object
                      properties:
                        id:
                          type: string
                          description: The device that page is about.
                      required:
                        - id
                      additionalProperties: true
                    run:
                      type: object
                      properties:
                        id:
                          type: string
                          description: The agent run that page is about.
                      required:
                        - id
                      additionalProperties: true
                    selection:
                      type: object
                      properties:
                        deviceIds:
                          type: array
                          items:
                            type: string
                          description: >-
                            Devices ticked on a list page. More than one always
                            asks which comes first.
                      required:
                        - deviceIds
                      additionalProperties: true
                    pinned:
                      type: object
                      properties:
                        deviceId:
                          type: string
                          description: >-
                            A device pinned to this thread. Outranks every other
                            source.
                      required:
                        - deviceId
                      additionalProperties: true
                  required: []
                  additionalProperties: true
                  title: CopilotContext
                  description: >-
                    What the console knows and the sentence does not say. Every
                    device id here must be one your session can address, or the
                    request is 400 naming the field. Resolution order for which
                    phone a turn is about: `pinned`, then `mentions`, then
                    `context.device`, then the device this thread last acted on,
                    then the only device you have, then a `data-device-choice`
                    card. THIS BLOCK IS NOT REPLAYED: it describes where
                    somebody is now, so it is read from this request only and
                    never from the stored thread.
                mentions:
                  type: array
                  maxItems: 10
                  items:
                    type: string
                  description: >-
                    Device ids the person named with an `@` in this message. The
                    id travels, never the typed name, so renaming a phone cannot
                    re-point a message. Exactly one names the turn's device; two
                    or more raise the chooser.
                threadId:
                  type: string
                  description: >-
                    Append the result to this thread, as a tool part. Required
                    for a command that asks first.
                  pattern: ^[A-Za-z0-9_-]{1,128}$
                approvalId:
                  type: string
                  description: >-
                    The id from a 202 this route returned, sent with the same
                    tool and input to run it.
              required:
                - tool
              additionalProperties: false
              title: CopilotCommandBody
      responses:
        '200':
          description: >-
            The tool ran. `isError` is the tool's own verdict, not an HTTP
            status: a refusal by the device is a 200 with `isError` true.
          content:
            application/json:
              schema:
                type: object
                properties:
                  tool:
                    type: string
                    description: The tool that ran.
                  isError:
                    type: boolean
                    description: The tool's own verdict.
                  output:
                    type: object
                    properties:
                      summary:
                        type: string
                        description: A bounded line, as a chat bubble shows it.
                      text:
                        type: string
                        description: The tool's whole answer.
                      frameId:
                        type: string
                        description: The frame a screenshot produced, when it named one.
                    required:
                      - summary
                      - text
                    additionalProperties: false
                    title: CopilotCommandOutput
                  files:
                    type: array
                    description: >-
                      Pictures the tool returned, as `data:` URLs, when small
                      enough to carry inline.
                    items:
                      type: object
                      properties:
                        mediaType:
                          type: string
                          description: The media type.
                        url:
                          type: string
                          description: A `data:` URL.
                      required:
                        - mediaType
                        - url
                      additionalProperties: false
                  threadId:
                    type: string
                    description: The thread the result was appended to, when one was named.
                  messageId:
                    type: string
                    description: The assistant message the result was written as.
                required:
                  - tool
                  - isError
                  - output
                additionalProperties: false
        '202':
          description: >-
            It asks first. Nothing ran. Show the card, then send the same tool
            and input again with the `approvalId`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  approval:
                    type: object
                    properties:
                      tool:
                        type: string
                        description: The tool the question is about.
                      arguments:
                        type: object
                        additionalProperties: true
                        description: The arguments it would run with.
                      prompt:
                        type: string
                        description: >-
                          One sentence naming the effect, built from the tool
                          and its arguments and never from model prose.
                    required:
                      - tool
                      - arguments
                      - prompt
                    additionalProperties: false
                    title: CopilotCommandApproval
                  approvalId:
                    type: string
                    description: Send this back with the same tool and input.
                  toolCallId:
                    type: string
                    description: The call the question is about, as the thread stores it.
                required:
                  - approval
                  - approvalId
                  - toolCallId
                additionalProperties: false
        '400':
          description: >-
            An unknown tool, a tool that is not a direct command (ask in the
            conversation), a context naming a device you cannot address, a tool
            that needs a device when nothing named one, or a gated command with
            no `threadId` to hold its card.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '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: >-
            An API key, or a tool your session has no scope for. The message
            names the scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            The thread is not yours, or no question is waiting on that
            `approvalId` for this command.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotFoundError'
        '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: This deployment serves no direct commands.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServiceUnavailableError'
      security:
        - bearerAuth: []
components:
  schemas:
    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.
    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.
    NotFoundError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              const: not_found
              description: Always `not_found` on this response.
            message:
              type: string
              description: >-
                A human readable summary naming the kind of thing that was not
                found.
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      description: >-
        No such thing that you may act on. Not an existence oracle over other
        organisations' ids.
    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.
    ServiceUnavailableError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              const: service_unavailable
              description: Always `service_unavailable` on this response.
            message:
              type: string
              description: >-
                A human readable summary naming the surface this deployment does
                not serve.
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      description: >-
        This deployment does not serve that surface. No `Retry-After`: waiting
        does not help.
  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.