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

# Ask the copilot

> One copilot turn, streamed. Two protocols answer on this route for one release, chosen by `Accept`.

THE UI MESSAGE STREAM. List `application/vnd.phonebase.ui-message-stream` in `Accept` and send the AI SDK transport's body: `{id, messages, trigger, messageId}`, where `id` is the thread id (a new one opens the thread) and `messages` is the client's transcript. ONLY THE LAST MESSAGE IS READ: a user message (its text parts) starts a turn, and an assistant message carrying a tool part in state `approval-responded` answers the question the previous turn ended on. The history the model is shown is the STORED thread, within a budget of 48000 characters of transcript, never the client's copy. What the budget cuts away is NOT forgotten: it is folded into a short summary the copilot writes once and keeps with the thread, shown in front of the recent messages on every turn after that, so a long conversation still knows what it was started for and which phone it was about. Nothing rebuildable from the server goes into that summary. The response is `text/event-stream` with the header `x-vercel-ai-ui-message-stream: v1`, one `data: <chunk>` frame per chunk, ended by `data: [DONE]`. It opens with `start` (naming the assistant message being written, which is the message a resumed approval continues), carries text and reasoning as deltas, every tool call as `tool-input-available` before it runs and `tool-output-available` (`{summary, text, frameId?}`), `tool-output-error` (`errorText`, which the protocol requires, and when the tool itself failed also `{message, code, retryable, retryAfterMs?}` beside it, on the stream only and never on a stored part) or `tool-output-denied` after, a `file` chunk with a `data:` URL for a screenshot of at most 204800 bytes, `data-allowance` ({percent, metered}; TRANSIENT, delivered to the client's data callback and never stored as a part) after `start` and before `finish`, `data-device-choice` ({reason, devices:[{id, label}]}) when a call needed a phone and none was named, `data-suggestions` ({items:[{text, deviceId}]}, at most three, each naming a device) before `finish` on a turn that completed -- both of those ARE parts and survive a replay of the thread, and ends with `finish` (`finishReason`, and `messageMetadata.stopReason` from the copilot's own closed set) or with `error`, after which nothing follows.

WHEN IT FAILS (API-232). `copilot_unfunded` means the copilot has no credit ON OUR SIDE: the message is the sentence to show, nothing the caller did caused it, and their phone and direct commands are unaffected. `rate_limited` is the provider throttling and is worth retrying. `model_unavailable` is the model being unreachable. Those three were ONE code until now, so a client could only ever say the vaguest of them; `model_refused`, `tool_failed` and `internal` complete the set. The older event stream still reports the first three as `model_unavailable`, because its console branches on the set as it stood.

APPROVALS RIDE THE STREAM. Anything that changes the phone, takes or hands back the device, or starts an agent run asks first: `tool-approval-request` carries an `approvalId` and a descriptor with the tool, its arguments and one sentence to show, and the stream ENDS with `finish{finishReason: "tool-calls"}`. The descriptor says which card to draw and why: `kind` is `light` (one line and one row) or `plan` (a goal with an editable `budget` of `maxSteps` and `deadlineMs`), and `reason.code` is `start_run`, `install_app`, `dangerous_operation` or `boundary`. When one question covers the same tool on SEVERAL phones the descriptor carries `calls: [{toolCallId, tool, input}]` and one yes runs all of them, each as its own call with its own receipt; a dangerous operation is never folded into that list and always gets its own card.

Answer by sending the same thread again with the assistant message's tool part in state `approval-responded` (`approval: {id, approved, reason?, input?, descriptor?}`); the next stream continues the same assistant message: the tool runs, or `tool-output-denied` ends the turn. `approval.input` is the person's EDIT of the arguments on a plan card: it is MERGED over the arguments they were shown, so sending only the field they changed does not delete the rest, and it is re-checked against the tool's published input schema. An edit the schema refuses is 400 naming the field, and nothing runs. If you echo `approval.descriptor.reason`, it must be the code the question was asked with, or the answer is 400: consent shown for one reason is not consent for another. Echoing nothing is accepted. The question does NOT expire on a clock: it waits until you answer it (Confirm or Decline) or a new message on the thread supersedes it, which closes it with `tool-output-error`. A turn takes at most 12 model steps, across the requests an approval splits it into.

Closing the request aborts the turn: the copilot stops before its next step or tool call, what was written is kept, and the stored message ends with `finishReason: "stop"`. A tool already running completes and its receipt stands.

THE EVENT STREAM (older). Any other `Accept`, or none, selects it. Send `{prompt}`; the response is a `text/event-stream` whose frames are named SSE events: `session.start` once, first; `message.delta` prose; `tool.call` a tool is ABOUT TO RUN; `tool.result` it finished, `isError` being the tool's own verdict; `approval.request` a question, the turn PAUSED; `tool.denied` you said no or nobody answered; `ping` keep-alive; `done` with a `stopReason`; `error` with a `code`. Exactly one `done` or one `error` ends every stream. Approvals are answered on `POST /v1/copilot/approvals`, a SEPARATE request, while the stream stays open; a client that closes the stream to answer loses the turn. This protocol goes in the release after the console has switched.

WHICH PHONE (API-228). Both shapes take an optional `context` and `mentions`. When they name a device, the turn is about it: the model is told so, a call that needs a device and names none is given that one, and it does not ask which phone. When nothing names one and you have several, a `data-device-choice` chunk carries the candidates, the call is answered `tool-output-error`, and no phone is touched. A device id in `context` or `mentions` that your session cannot address is 400 naming the field; every wrong field is named in the one answer.

Needs a signed in person. A copilot acts as somebody, and an API key has nobody behind it; drive the tools directly through the MCP endpoint instead. The turn can reach exactly the tools your session already has, no more, because it drives the same tool surface this service publishes.

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
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:
    post:
      tags:
        - Copilot
      summary: Ask the copilot
      description: >-
        One copilot turn, streamed. Two protocols answer on this route for one
        release, chosen by `Accept`.


        THE UI MESSAGE STREAM. List
        `application/vnd.phonebase.ui-message-stream` in `Accept` and send the
        AI SDK transport's body: `{id, messages, trigger, messageId}`, where
        `id` is the thread id (a new one opens the thread) and `messages` is the
        client's transcript. ONLY THE LAST MESSAGE IS READ: a user message (its
        text parts) starts a turn, and an assistant message carrying a tool part
        in state `approval-responded` answers the question the previous turn
        ended on. The history the model is shown is the STORED thread, within a
        budget of 48000 characters of transcript, never the client's copy. What
        the budget cuts away is NOT forgotten: it is folded into a short summary
        the copilot writes once and keeps with the thread, shown in front of the
        recent messages on every turn after that, so a long conversation still
        knows what it was started for and which phone it was about. Nothing
        rebuildable from the server goes into that summary. The response is
        `text/event-stream` with the header `x-vercel-ai-ui-message-stream: v1`,
        one `data: <chunk>` frame per chunk, ended by `data: [DONE]`. It opens
        with `start` (naming the assistant message being written, which is the
        message a resumed approval continues), carries text and reasoning as
        deltas, every tool call as `tool-input-available` before it runs and
        `tool-output-available` (`{summary, text, frameId?}`),
        `tool-output-error` (`errorText`, which the protocol requires, and when
        the tool itself failed also `{message, code, retryable, retryAfterMs?}`
        beside it, on the stream only and never on a stored part) or
        `tool-output-denied` after, a `file` chunk with a `data:` URL for a
        screenshot of at most 204800 bytes, `data-allowance` ({percent,
        metered}; TRANSIENT, delivered to the client's data callback and never
        stored as a part) after `start` and before `finish`,
        `data-device-choice` ({reason, devices:[{id, label}]}) when a call
        needed a phone and none was named, `data-suggestions` ({items:[{text,
        deviceId}]}, at most three, each naming a device) before `finish` on a
        turn that completed -- both of those ARE parts and survive a replay of
        the thread, and ends with `finish` (`finishReason`, and
        `messageMetadata.stopReason` from the copilot's own closed set) or with
        `error`, after which nothing follows.


        WHEN IT FAILS (API-232). `copilot_unfunded` means the copilot has no
        credit ON OUR SIDE: the message is the sentence to show, nothing the
        caller did caused it, and their phone and direct commands are
        unaffected. `rate_limited` is the provider throttling and is worth
        retrying. `model_unavailable` is the model being unreachable. Those
        three were ONE code until now, so a client could only ever say the
        vaguest of them; `model_refused`, `tool_failed` and `internal` complete
        the set. The older event stream still reports the first three as
        `model_unavailable`, because its console branches on the set as it
        stood.


        APPROVALS RIDE THE STREAM. Anything that changes the phone, takes or
        hands back the device, or starts an agent run asks first:
        `tool-approval-request` carries an `approvalId` and a descriptor with
        the tool, its arguments and one sentence to show, and the stream ENDS
        with `finish{finishReason: "tool-calls"}`. The descriptor says which
        card to draw and why: `kind` is `light` (one line and one row) or `plan`
        (a goal with an editable `budget` of `maxSteps` and `deadlineMs`), and
        `reason.code` is `start_run`, `install_app`, `dangerous_operation` or
        `boundary`. When one question covers the same tool on SEVERAL phones the
        descriptor carries `calls: [{toolCallId, tool, input}]` and one yes runs
        all of them, each as its own call with its own receipt; a dangerous
        operation is never folded into that list and always gets its own card.


        Answer by sending the same thread again with the assistant message's
        tool part in state `approval-responded` (`approval: {id, approved,
        reason?, input?, descriptor?}`); the next stream continues the same
        assistant message: the tool runs, or `tool-output-denied` ends the turn.
        `approval.input` is the person's EDIT of the arguments on a plan card:
        it is MERGED over the arguments they were shown, so sending only the
        field they changed does not delete the rest, and it is re-checked
        against the tool's published input schema. An edit the schema refuses is
        400 naming the field, and nothing runs. If you echo
        `approval.descriptor.reason`, it must be the code the question was asked
        with, or the answer is 400: consent shown for one reason is not consent
        for another. Echoing nothing is accepted. The question does NOT expire
        on a clock: it waits until you answer it (Confirm or Decline) or a new
        message on the thread supersedes it, which closes it with
        `tool-output-error`. A turn takes at most 12 model steps, across the
        requests an approval splits it into.


        Closing the request aborts the turn: the copilot stops before its next
        step or tool call, what was written is kept, and the stored message ends
        with `finishReason: "stop"`. A tool already running completes and its
        receipt stands.


        THE EVENT STREAM (older). Any other `Accept`, or none, selects it. Send
        `{prompt}`; the response is a `text/event-stream` whose frames are named
        SSE events: `session.start` once, first; `message.delta` prose;
        `tool.call` a tool is ABOUT TO RUN; `tool.result` it finished, `isError`
        being the tool's own verdict; `approval.request` a question, the turn
        PAUSED; `tool.denied` you said no or nobody answered; `ping` keep-alive;
        `done` with a `stopReason`; `error` with a `code`. Exactly one `done` or
        one `error` ends every stream. Approvals are answered on `POST
        /v1/copilot/approvals`, a SEPARATE request, while the stream stays open;
        a client that closes the stream to answer loses the turn. This protocol
        goes in the release after the console has switched.


        WHICH PHONE (API-228). Both shapes take an optional `context` and
        `mentions`. When they name a device, the turn is about it: the model is
        told so, a call that needs a device and names none is given that one,
        and it does not ask which phone. When nothing names one and you have
        several, a `data-device-choice` chunk carries the candidates, the call
        is answered `tool-output-error`, and no phone is touched. A device id in
        `context` or `mentions` that your session cannot address is 400 naming
        the field; every wrong field is named in the one answer.


        Needs a signed in person. A copilot acts as somebody, and an API key has
        nobody behind it; drive the tools directly through the MCP endpoint
        instead. The turn can reach exactly the tools your session already has,
        no more, because it drives the same tool surface this service publishes.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: postV1Copilot
      parameters:
        - name: Accept
          in: header
          required: false
          description: >-
            List `application/vnd.phonebase.ui-message-stream` to be answered
            with the UI message stream. Anything else, or no header, selects the
            older event stream. The response is `text/event-stream` either way.
          schema:
            type: string
      requestBody:
        description: What to ask, in the shape of the protocol selected by `Accept`.
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  properties:
                    prompt:
                      type: string
                      minLength: 1
                      maxLength: 4000
                      description: >-
                        What you want, in a sentence. A turn is a message, not a
                        document. The older protocol's body.
                    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.
                  required:
                    - prompt
                  additionalProperties: false
                  title: PromptBody
                - type: object
                  properties:
                    id:
                      type: string
                      description: >-
                        The thread id. A new id opens a new thread owned by you;
                        an id that is not your live thread answers 404.
                      pattern: ^[A-Za-z0-9_-]{1,128}$
                    messages:
                      type: array
                      minItems: 1
                      items:
                        $ref: '#/components/schemas/ChatMessage'
                      description: >-
                        The client's transcript. Only the LAST message is read:
                        a user message with text parts of at most 4000
                        characters together, or an assistant message carrying an
                        approval answer.
                    trigger:
                      type: string
                      enum:
                        - submit-message
                        - regenerate-message
                      description: >-
                        Read only when the last message's id is one this thread
                        already stores (see 409). `regenerate-message` then asks
                        for a new answer to the newest question, without storing
                        the question again; anything else gets the stored answer
                        back.
                    messageId:
                      type: string
                      description: Accepted and not read.
                    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.
                  required:
                    - id
                    - messages
                  additionalProperties: true
                  title: ChatTransportBody
      responses:
        '200':
          description: >-
            The stream. It stays open until the turn ends. Which protocol it
            speaks is in the header below.
          headers:
            x-vercel-ai-ui-message-stream:
              description: >-
                `v1` when the UI message stream was selected; absent on the
                older event stream.
              schema:
                type: string
          content:
            text/event-stream: {}
        '400':
          description: >-
            A missing or unusable `prompt`, a transport body whose last message
            carries neither text nor an approval answer, a `context` /
            `mentions` block naming a device this session cannot address, an
            `approval.input` edit the tool's schema refuses, or an echoed
            `approval.descriptor.reason.code` that is not the one the question
            was asked with. The message names every field that is wrong.
          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. A copilot acts as a person, and a key is nobody.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            The thread id is not your live thread, or no approval is waiting on
            the id you answered.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotFoundError'
        '409':
          description: >-
            `idempotency_conflict`. The last message's `id` is the idempotency
            key of that message: a retry under the same id never stores the
            question twice. Sent again once it has an answer, the stored answer
            is streamed back and nothing runs (`regenerate-message` on the
            newest question runs a new answer instead). 409 when the id already
            names DIFFERENT content in this thread (`retryable: false`; send new
            words under a new id), or when the first request for that message is
            still being answered (`retryable: true`; wait, or read it back from
            `GET /v1/copilot/threads/{id}/stream`).
          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: This deployment has no copilot configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServiceUnavailableError'
      security:
        - bearerAuth: []
components:
  schemas:
    ChatMessage:
      type: object
      properties:
        id:
          type: string
          description: >-
            Assigned by this service. The id a client sent for a user message is
            not kept.
        role:
          type: string
          enum:
            - user
            - assistant
          description: Who wrote it. There are no system messages in a thread.
        parts:
          type: array
          description: >-
            The message as parts, in the AI SDK's UIMessage shape and order:
            `text`, `reasoning`, `step-start`, `file`, `data-*` (the device
            chooser and the follow-up suggestions; the allowance is transient
            and is never a part), and `tool-<name>` parts carrying `toolCallId`,
            `input`, a `state` and, by state, `output`, `errorText` or
            `approval`. A thread replays into a chat client without translation.


            An `approval` carries `{id, descriptor}` while the question is open
            and `{id, approved, reason?, input?, descriptor?}` once it is
            answered, and keeps those fields after the call has run: `input` is
            the edit the person sent with the answer, so a settled card still
            shows which edited values it ran with. The descriptor is `{tool,
            arguments, prompt, kind, reason:{code}, summary?, budget?, calls?,
            runHold?, expiresAt?}`: `kind` is `light` or `plan`, `reason.code`
            is one of `start_run`, `install_app`, `dangerous_operation`,
            `boundary`, and `calls` is present only when one card covers several
            phones. `runHold` is `{runId, pausedAt}` on a `resume_run` card: the
            run and the hold (`pausedAt`, or null for not paused) the card
            showed. Descriptors written before those fields existed carry
            neither `kind` nor `reason`; read a missing `kind` as `light`.
          items:
            type: object
        metadata:
          type: object
          properties:
            createdAt:
              type: string
              format: date-time
            finishReason:
              type: string
              description: >-
                How an assistant message ended, in the stream's words: `stop`,
                `tool-calls`, `length`, `other` or `error`. Absent on a user
                message.
            stopReason:
              type: string
              enum:
                - completed
                - max_steps
                - approval_expired
                - denied
                - awaiting_approval
                - aborted
              description: >-
                The copilot's own reason, finer than finishReason. Absent on a
                user message.
          required:
            - createdAt
          additionalProperties: false
      required:
        - id
        - role
        - parts
        - metadata
      additionalProperties: false
    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.