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

# One thread, with a page of its messages

> The thread and its newest messages, oldest first, as UI messages a chat client renders without translation. A tool part in state `approval-requested` on the last message is a question still waiting: answer it on POST /v1/copilot.

PAGED, NEWEST FIRST. The response carries the newest `limit` messages; `nextBefore` names the oldest of them when older ones exist, and sending it back as `before` returns the page before that. A thread is heavy in a way a listing is not: every tool result carries its full text and every screenshot its bytes.

Inline screenshots (`file` parts with a `data:` URL) are carried within a budget of 4194304 DECODED bytes per response (about 1.33 times that on the wire, as base64 inside JSON). The fit is greedy, newest first: each screenshot is kept if it fits what is left, so a large one that does not fit is left out while a smaller older one may still be carried. `files=none` leaves every one out. `omittedFiles` counts what was left out, and the `frameId` on the tool output beside each one still names the picture for the frames API.

A thread that is not yours, one that was deleted, and one that never existed all answer 404 alike.

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



## OpenAPI

````yaml /api/openapi.json get /v1/copilot/threads/{id}
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/threads/{id}:
    get:
      tags:
        - Copilot
      summary: One thread, with a page of its messages
      description: >-
        The thread and its newest messages, oldest first, as UI messages a chat
        client renders without translation. A tool part in state
        `approval-requested` on the last message is a question still waiting:
        answer it on POST /v1/copilot.


        PAGED, NEWEST FIRST. The response carries the newest `limit` messages;
        `nextBefore` names the oldest of them when older ones exist, and sending
        it back as `before` returns the page before that. A thread is heavy in a
        way a listing is not: every tool result carries its full text and every
        screenshot its bytes.


        Inline screenshots (`file` parts with a `data:` URL) are carried within
        a budget of 4194304 DECODED bytes per response (about 1.33 times that on
        the wire, as base64 inside JSON). The fit is greedy, newest first: each
        screenshot is kept if it fits what is left, so a large one that does not
        fit is left out while a smaller older one may still be carried.
        `files=none` leaves every one out. `omittedFiles` counts what was left
        out, and the `frameId` on the tool output beside each one still names
        the picture for the frames API.


        A thread that is not yours, one that was deleted, and one that never
        existed all answer 404 alike.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: getV1CopilotThreadsById
      parameters:
        - name: id
          in: path
          required: true
          description: The thread id.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: >-
            How many messages, 1 to 200. Defaults to 50. Above the maximum is
            refused, never quietly reduced.
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - name: before
          in: query
          required: false
          description: >-
            A message id from a previous page's `nextBefore`: the page before
            that message. Omit it for the newest page.
          schema:
            type: string
        - name: files
          in: query
          required: false
          description: >-
            `inline` (the default) carries screenshots as `data:` URLs within
            the byte budget; `none` leaves every one out.
          schema:
            type: string
            enum:
              - inline
              - none
      responses:
        '200':
          description: The thread and one page of its messages.
          content:
            application/json:
              schema:
                type: object
                properties:
                  thread:
                    $ref: '#/components/schemas/CopilotThread'
                  mode:
                    type:
                      - string
                      - 'null'
                    enum:
                      - auto
                      - ask
                      - manual
                      - null
                    description: >-
                      This thread's saved automation mode, or null when none was
                      chosen. Read it when reopening the chat on another
                      browser.
                  messages:
                    type: array
                    items:
                      $ref: '#/components/schemas/ChatMessage'
                    description: Oldest first, newest last.
                  omittedFiles:
                    type: integer
                    minimum: 0
                    description: >-
                      Inline screenshots left out of this page, by `files=none`
                      or the byte budget.
                  nextBefore:
                    type: string
                    description: >-
                      The oldest message id on this page, to send as `before`
                      for the page before it. Absent when this page reached the
                      start.
                required:
                  - thread
                  - mode
                  - messages
                  - omittedFiles
                additionalProperties: false
        '400':
          description: >-
            `limit` out of range, `before` not a message id, or `files` not one
            of the two.
          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. Threads belong to people.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No thread with that id for you.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotFoundError'
        '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'
      security:
        - bearerAuth: []
components:
  schemas:
    CopilotThread:
      type: object
      properties:
        id:
          type: string
          description: >-
            The thread id. Minted by the client when the conversation opened;
            the same id is `id` on POST /v1/copilot.
        title:
          type: string
          description: >-
            The first 60 characters of the first message, or the name given on
            PATCH.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        lastMessageAt:
          type: string
          format: date-time
          description: >-
            When the newest message was written. The listing orders on it,
            newest first.
        messageCount:
          type: integer
          minimum: 0
          description: Messages in the thread, user and assistant together.
      required:
        - id
        - title
        - createdAt
        - updatedAt
        - lastMessageAt
        - messageCount
      additionalProperties: false
    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.
    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.