> ## 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 the org's keys

> Your org's keys, newest first, one page at a time. Secrets are not stored in recoverable form and are never in this response. `lastUsedAt` is written off the request path, so it can lag slightly behind the last real use, which is worth knowing before you read a quiet key as unused.

PAGING. A full page carries `nextBefore`; send it back as `before` to get the next (older) page, and keep going until it is absent. Its absence is how paging ends, and it is the only reliable signal: a page that happens to be exactly full is not the end. This listing previously answered a bare array truncated at 200 rows with no cursor and no flag, so an org at the ceiling and an org far past it were indistinguishable.

WHO MAY READ IT. Any signed-in principal in the org, of either role. An ADMIN receives the full summary. A MEMBER receives the same keys, in the same order, with the same cursor, but a narrower projection: what a key IS (its ids, name, scopes, device narrowing, validity window and who created it) is returned, and how it has been USED is not, so `lastUsedAt` and `rotatedFrom` are withheld. A member can therefore see that their org has keys, match one they hold by its `keyId` to read why it does not work, and tell which rows are theirs to manage, without being handed the fields that rank an org's credentials by how useful one would be to take away.

`createdBy` is an id and not a name; resolve it through your own directory. It is in the member projection because a member may manage the keys they created and would otherwise have no way to tell which those are.

An API key cannot read this listing at all, whatever the role of whoever minted it.

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/api-keys
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/api-keys:
    get:
      tags:
        - API keys
      summary: List the org's keys
      description: >-
        Your org's keys, newest first, one page at a time. Secrets are not
        stored in recoverable form and are never in this response. `lastUsedAt`
        is written off the request path, so it can lag slightly behind the last
        real use, which is worth knowing before you read a quiet key as unused.


        PAGING. A full page carries `nextBefore`; send it back as `before` to
        get the next (older) page, and keep going until it is absent. Its
        absence is how paging ends, and it is the only reliable signal: a page
        that happens to be exactly full is not the end. This listing previously
        answered a bare array truncated at 200 rows with no cursor and no flag,
        so an org at the ceiling and an org far past it were indistinguishable.


        WHO MAY READ IT. Any signed-in principal in the org, of either role. An
        ADMIN receives the full summary. A MEMBER receives the same keys, in the
        same order, with the same cursor, but a narrower projection: what a key
        IS (its ids, name, scopes, device narrowing, validity window and who
        created it) is returned, and how it has been USED is not, so
        `lastUsedAt` and `rotatedFrom` are withheld. A member can therefore see
        that their org has keys, match one they hold by its `keyId` to read why
        it does not work, and tell which rows are theirs to manage, without
        being handed the fields that rank an org's credentials by how useful one
        would be to take away.


        `createdBy` is an id and not a name; resolve it through your own
        directory. It is in the member projection because a member may manage
        the keys they created and would otherwise have no way to tell which
        those are.


        An API key cannot read this listing at all, whatever the role of whoever
        minted it.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: getV1ApiKeys
      parameters:
        - name: limit
          in: query
          required: false
          description: >-
            How many keys to return, 1 to 200. A larger value is refused, not
            shortened, so a caller is never left believing they received
            everything they asked for.
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - name: before
          in: query
          required: false
          description: >-
            Opaque cursor from a previous page's `nextBefore`. Returns only keys
            older than it. Treat it as opaque: its form is not part of this
            contract. An empty value is refused rather than read as no cursor,
            because that reads back as an org with no keys.
          schema:
            type: string
      responses:
        '200':
          description: A page of the org's keys.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyPage'
        '400':
          description: '`limit` was out of range, or `before` was empty.'
          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 cannot read key management; use a signed-in principal.
          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:
    ApiKeyPage:
      type: object
      properties:
        keys:
          type: array
          items:
            $ref: '#/components/schemas/ApiKeySummary'
          maxItems: 200
          description: >-
            For an admin, the full summary. For a member, the same entries
            without `lastUsedAt` or `rotatedFrom`.
        nextBefore:
          type: string
          description: >-
            Send back as `before` for the next page. ABSENT on the last page,
            which is how paging ends.
      required:
        - keys
      additionalProperties: false
      description: A page of API keys, 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.
    ApiKeySummary:
      type: object
      properties:
        apiKeyId:
          type: string
          description: The key's own id. This is what you pass to revoke it.
        keyId:
          type: string
          description: The public half of the token, the part before the secret.
        name:
          type: string
        scopes:
          type: array
          items:
            type: string
            enum:
              - devices:read
              - devices:act
              - devices:lease
              - runs:start
              - orders:place
              - apps:read
              - apps:write
              - apps:delete
            description: A granted scope.
        deviceIds:
          type:
            - array
            - 'null'
          items:
            type: string
          description: >-
            The device subset this key may see and address, or null for the
            whole org.
        createdBy:
          type: string
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type:
            - string
            - 'null'
          format: date-time
        revokedAt:
          type:
            - string
            - 'null'
          format: date-time
        lastUsedAt:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            Updated off the request path, so it can lag slightly behind the last
            real use.
        rotatedFrom:
          type:
            - string
            - 'null'
          description: >-
            The apiKeyId this key replaced via rotation, or null for keys minted
            directly.
      required:
        - apiKeyId
        - keyId
        - name
        - scopes
        - deviceIds
        - createdBy
        - createdAt
        - expiresAt
        - revokedAt
        - lastUsedAt
        - rotatedFrom
      additionalProperties: false
      description: >-
        A stored key, as a listing returns it. The secret is not part of it and
        cannot be recovered.
  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.