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

# List your org's receipts

> Every action your org has taken, newest first. This is the enumeration counterpart to the `get_receipt` tool: use that one to recover a lost response, and this one to answer what has happened.

TWO DIFFERENCES FROM `get_receipt`, both worth knowing before you reach for it. It answers about ONE key rather than a page, and it requires the `deviceId` as well as the key. This route and `GET /v1/receipts/{idempotencyKey}` do not: an idempotency key is unique PER DEVICE, so one organisation can legitimately hold the same key on two devices, and these two resolve that by answering 400 and naming `deviceId` only when it actually happens. So a recovery path that does not know which device it sent to has to come here, not to the tool. Narrowing the tool to match is filed as D170 and is not in this release.

Paging is keyset based, not offset based, so rows cannot be skipped or repeated while new receipts arrive. When a page is not the last, the response carries `nextBefore` and `nextBeforeKey`; send them back as `before` and `beforeKey` for the next page, and stop when they are absent. Send BOTH, and send them unchanged: the ordering is by timestamp and key together, and receipts written concurrently routinely share a timestamp.

A key narrowed to a subset of devices sees only that subset. Asking about a device outside it returns an empty page rather than a refusal, which is the same shape a narrowed key already sees in device listings: out of subset reads as nothing here, never as exists but denied.

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/receipts
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/receipts:
    get:
      tags:
        - Usage
      summary: List your org's receipts
      description: >-
        Every action your org has taken, newest first. This is the enumeration
        counterpart to the `get_receipt` tool: use that one to recover a lost
        response, and this one to answer what has happened.


        TWO DIFFERENCES FROM `get_receipt`, both worth knowing before you reach
        for it. It answers about ONE key rather than a page, and it requires the
        `deviceId` as well as the key. This route and `GET
        /v1/receipts/{idempotencyKey}` do not: an idempotency key is unique PER
        DEVICE, so one organisation can legitimately hold the same key on two
        devices, and these two resolve that by answering 400 and naming
        `deviceId` only when it actually happens. So a recovery path that does
        not know which device it sent to has to come here, not to the tool.
        Narrowing the tool to match is filed as D170 and is not in this release.


        Paging is keyset based, not offset based, so rows cannot be skipped or
        repeated while new receipts arrive. When a page is not the last, the
        response carries `nextBefore` and `nextBeforeKey`; send them back as
        `before` and `beforeKey` for the next page, and stop when they are
        absent. Send BOTH, and send them unchanged: the ordering is by timestamp
        and key together, and receipts written concurrently routinely share a
        timestamp.


        A key narrowed to a subset of devices sees only that subset. Asking
        about a device outside it returns an empty page rather than a refusal,
        which is the same shape a narrowed key already sees in device listings:
        out of subset reads as nothing here, never as exists but denied.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: getV1Receipts
      parameters:
        - name: deviceId
          in: query
          required: false
          description: >-
            Only receipts for this device. Omit for every device the credential
            can see.
          schema:
            type: string
        - name: agentRunId
          in: query
          required: false
          description: >-
            Only receipts produced by this agent run, which is how you rebuild
            one run's action sequence without pulling every receipt for the
            device and filtering client side. Matching is on the run's own
            receipts, not on a name prefix, so a run id that begins with another
            run's id does not collect its receipts. An empty value is refused
            rather than answered with everything.
          schema:
            type: string
            minLength: 1
        - name: status
          in: query
          required: false
          description: >-
            Only receipts in this state. `failed` is the usual one: it is how
            you list what went wrong without reading everything that went right.
          schema:
            type: string
            enum:
              - pending
              - committed
              - failed
            description: A receipt state.
        - name: actorUserId
          in: query
          required: false
          description: >-
            Only actions taken by this person, by the subject id you read off
            `actor.userId`. Mutually exclusive with `actorApiKeyId`: an actor is
            a person or a key and never both, so asking for both is refused
            rather than answered with an empty page. An empty value is refused
            too.
          schema:
            type: string
            minLength: 1
        - name: actorApiKeyId
          in: query
          required: false
          description: >-
            Only actions taken by this key, by the id you read off
            `actor.apiKeyId` or from `GET /v1/api-keys`. This is the answer to
            "a key was public for eleven hours, what did it do". Mutually
            exclusive with `actorUserId`.
          schema:
            type: string
            minLength: 1
        - name: limit
          in: query
          required: false
          description: >-
            How many receipts to return. Defaults to 50; larger values are
            clamped to 200 rather than refused.
          schema:
            type: integer
            minimum: 1
        - name: before
          in: query
          required: false
          description: >-
            Keyset cursor, first half: the `nextBefore` of the previous page,
            passed back unchanged. On its own it also works as a plain time
            bound, returning receipts recorded strictly before this timestamp.
          schema:
            type: string
            format: date-time
        - name: beforeKey
          in: query
          required: false
          description: >-
            Keyset cursor, second half: the `nextBeforeKey` of the previous
            page. Send it whenever you send `before` from a previous page.
            Paging with `before` alone cannot separate receipts that share a
            timestamp, and under concurrent dispatch many do.
          schema:
            type: string
      responses:
        '200':
          description: A page of receipts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceiptPage'
        '400':
          description: >-
            An unusable `limit`, `before`, `beforeKey`, `agentRunId` or
            `status`.
          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: Reading receipts needs `devices:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: >-
            Over the request budget for this caller. Hosted deployments only.
            Which budget applied depends on whether this request carried an
            `Authorization` header, not on which endpoint it was aimed at. Wait
            `Retry-After` seconds and retry the same request: the refusal
            happens before any work, so nothing was partially applied. See
            /api/rate-limits.
          headers:
            Retry-After:
              description: >-
                Whole seconds until one request is available again. Computed
                from the caller's own remaining allowance, not a fixed constant.
              schema:
                type: string
            RateLimit-Limit:
              description: >-
                The budget in effect for this caller class, in requests per
                minute.
              schema:
                type: string
            RateLimit-Remaining:
              description: >-
                Whole requests that were still available when this request was
                admitted. Zero on a 429.
              schema:
                type: string
            RateLimit-Reset:
              description: >-
                Seconds until the allowance is back to full. Longer than
                `Retry-After`, which is the wait for a single request.
              schema:
                type: string
            RateLimit-Policy:
              description: The budget and its window, as `<limit>;w=60`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitedError'
        '503':
          description: >-
            The authentication backend could not be reached. The service fails
            closed: an infrastructure fault is refused, never waved through.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
components:
  schemas:
    ReceiptPage:
      type: object
      properties:
        receipts:
          type: array
          items:
            $ref: '#/components/schemas/Receipt'
        nextBefore:
          type: string
          format: date-time
          description: >-
            Send back as `before` for the next page, TOGETHER with
            `nextBeforeKey`. ABSENT on the last page, which is how paging ends.
            It carries microsecond precision; pass it back unchanged rather than
            reformatting it, because a value rounded to milliseconds skips rows.
        nextBeforeKey:
          type: string
          description: >-
            The other half of the cursor. Send back as `beforeKey`. Receipts are
            ordered by timestamp AND key, so a cursor without this half cannot
            resume correctly when several receipts share a timestamp, which is
            the normal case under concurrent dispatch.
      required:
        - receipts
      additionalProperties: false
      description: A page of receipts, newest first.
    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.
    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.
    Receipt:
      type: object
      properties:
        orgId:
          type: string
        deviceId:
          type: string
        idempotencyKey:
          type: string
          description: The key the caller supplied, which is what makes a retry a replay.
        backendKind:
          type: string
        action:
          type: string
          description: Which action was attempted.
        argsSummary:
          type: object
          additionalProperties: true
          description: >-
            A summary of the arguments, not the raw input. Text is bounded and
            never echoed wholesale.
        status:
          type: string
          enum:
            - pending
            - committed
            - failed
          description: Where this action ended up.
        requestedAt:
          type: string
          format: date-time
        committedAt:
          type:
            - string
            - 'null'
          format: date-time
        failedAt:
          type:
            - string
            - 'null'
          format: date-time
        observedFrame:
          type: string
          description: The frame this action was bound to, when it was bound to one.
        error:
          type: object
          additionalProperties: true
          description: Present on a failed receipt.
        reason:
          type: string
          description: >-
            Why a failure was recorded, where the failure had a classified
            cause.
        requiresManualReview:
          type: boolean
          description: >-
            The physical outcome is unknown (a crash or a lost transport) and
            needs reconciliation.
        actor:
          $ref: '#/components/schemas/Actor'
        principalKind:
          type: string
          enum:
            - api_key
            - oauth
            - dev_token
            - system
            - unattributed
          description: >-
            SUPERSEDED by `actor.kind`, and identical to it. Read `actor` in new
            code; this stays because a consumer already reads it.
        apiKeyId:
          type: string
          description: >-
            SUPERSEDED by `actor.apiKeyId`, and identical to it. Read `actor` in
            new code.
        reviewedAt:
          type: string
          format: date-time
          description: >-
            When a person opened this receipt. Absent until one does, and fixed
            once set: the first instant is the answer to how long it waited. It
            records that somebody LOOKED, and not that the action was correct or
            accepted.
        reviewedBy:
          type: string
          description: >-
            Who looked. A subject id, never a name, and set together with
            `reviewedAt` or not at all.
      required:
        - orgId
        - deviceId
        - idempotencyKey
        - backendKind
        - action
        - argsSummary
        - status
        - requestedAt
        - actor
        - principalKind
      additionalProperties: false
      description: >-
        One recorded action, and who took it. `actor` names a person or a key by
        id, so a receipt can be attributed to the particular credential that
        made the call.


        THE LEASE THE ACTION RAN UNDER IS NOT PART OF THIS SHAPE. A receipt used
        to carry `leaseId` and `fencingToken`, and both are gone as of
        2026-09-12. They are credentials: a lease id is what releases a lease
        and a fencing token is what orders writes under it, which is why the
        `get_receipt` tool already withheld both and why `GET /v1/leases` names
        no lease id either. Reading receipts needs only `devices:read`, so
        publishing them here let a read-only credential lift a live lease off
        the newest receipt and hand it to one that can act. Nothing replaces
        them on this face.


        `principalKind` and `apiKeyId` beside it say the same thing in a flatter
        shape. They are not new: this endpoint has returned them since
        attribution was added, without declaring them here, and declaring them
        is part of the same change that added `actor`. New code should read
        `actor`, which is the only one of the two that can name a person.
    Actor:
      type: object
      properties:
        kind:
          type: string
          enum:
            - api_key
            - oauth
            - dev_token
            - system
            - unattributed
          description: >-
            Which credential this was. `system` is phonebase itself: a
            schedule's timer, or phonebase driving a run, and `onBehalfOf` then
            says for whom; it is never a person. `unattributed` means the record
            cannot say, which is NOT the same as a positive claim that no key
            was involved: it is what a row written before this field existed
            reads as.
        userId:
          type: string
          description: >-
            The authorization server's subject id of the PERSON, present only
            when `kind` is `oauth`. It is an id and never a name: resolve it
            through your own directory. Nothing here stores a name or an email
            address.
        apiKeyId:
          type: string
          description: >-
            The key's id, present only when `kind` is `api_key`. The same id
            `GET /v1/api-keys` lists.
        viaCopilot:
          type: boolean
          description: >-
            Whether a copilot did this on the named person's behalf. The person
            stays responsible: a label reads like "Mira Osei via copilot".
            ALWAYS PRESENT, and false today on every record, because there is no
            copilot yet. Present and false says so; an absent field would only
            mean an older server.
        onBehalfOf:
          type: object
          properties:
            kind:
              type: string
              enum:
                - schedule
                - user
                - api_key
                - dev_token
              description: >-
                Whose work it was: `schedule` (a timer fired), `user` or
                `api_key` (phonebase drove a run that person or key started), or
                `dev_token` (the local operator).
            scheduleId:
              type: string
              description: The schedule, when `kind` is `schedule`.
            userId:
              type: string
              description: >-
                The person's subject id, when `kind` is `user`. An id, never a
                name.
            apiKeyId:
              type: string
              description: The key's id, when `kind` is `api_key`.
          required:
            - kind
          additionalProperties: false
          description: >-
            Present exactly when `kind` is `system`: the control plane acted,
            and this says for whom. Never present on any other kind.
      required:
        - kind
        - viaCopilot
      additionalProperties: false
      description: >-
        WHO. The same shape answers three questions: who took an action
        (`Receipt.actor`), who is holding a device (`Lease.holder`), and who
        started a run (`RunView.initiator`). One shape on purpose, so the three
        can never disagree about what a given credential is called.
  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.