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

# What your organisation has bought, and where each purchase got to

> Your own organisation's device purchases, newest first. This is the read that lets a paid customer with no device yet be shown something other than an empty screen.

IT IS NOT `GET /v1/admission`. Admission says which screen a signed-in user sees; this says what was purchased and whether it arrived. They were the same question only while admission was the thing being sold.

WHEN NO DEVICE WAS FREE at the moment a purchase settled, the purchase still succeeds and comes back as `awaiting_provisioning` with an `estimatedBy` date. That is a commitment made to the customer, not an error state, and it is the case this endpoint exists for. The date can be moved by us, which is why it is an estimate; every move is recorded.

ANSWERS ONLY ABOUT THE CALLER'S OWN ORGANISATION, taken from the verified credential. An empty list is the organisation asking about itself and discloses nothing about anybody else.

MOUNTED ON EVERY HOSTED DEPLOYMENT, including one that is not finished being set up for provisioning: those answer 503 with `Retry-After`, not 404, for the reason given on `GET /v1/admission`.

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/fulfilments
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/fulfilments:
    get:
      tags:
        - Provisioning
      summary: What your organisation has bought, and where each purchase got to
      description: >-
        Your own organisation's device purchases, newest first. This is the read
        that lets a paid customer with no device yet be shown something other
        than an empty screen.


        IT IS NOT `GET /v1/admission`. Admission says which screen a signed-in
        user sees; this says what was purchased and whether it arrived. They
        were the same question only while admission was the thing being sold.


        WHEN NO DEVICE WAS FREE at the moment a purchase settled, the purchase
        still succeeds and comes back as `awaiting_provisioning` with an
        `estimatedBy` date. That is a commitment made to the customer, not an
        error state, and it is the case this endpoint exists for. The date can
        be moved by us, which is why it is an estimate; every move is recorded.


        ANSWERS ONLY ABOUT THE CALLER'S OWN ORGANISATION, taken from the
        verified credential. An empty list is the organisation asking about
        itself and discloses nothing about anybody else.


        MOUNTED ON EVERY HOSTED DEPLOYMENT, including one that is not finished
        being set up for provisioning: those answer 503 with `Retry-After`, not
        404, for the reason given on `GET /v1/admission`.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: getV1Fulfilments
      responses:
        '200':
          description: This organisation's purchases.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFulfilmentList'
        '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'
        '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 cannot answer for provisioning yet:
            `admission_unavailable` when the provisioning store cannot be read.
            Retryable, and `Retry-After` says how soon to ask again.
          headers:
            Retry-After:
              description: Seconds to wait before asking again.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisioningUnavailableError'
      security:
        - bearerAuth: []
components:
  schemas:
    DeviceFulfilmentList:
      type: object
      properties:
        fulfilments:
          type: array
          items:
            $ref: '#/components/schemas/DeviceFulfilment'
      required:
        - fulfilments
      additionalProperties: false
      description: This organisation's purchases, newest first.
    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.
    ProvisioningUnavailableError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - admission_unavailable
                - checkout_unavailable
              description: >-
                `admission_unavailable`: the provisioning store cannot be read
                right now; retry after `Retry-After`. `checkout_unavailable`:
                this deployment is not configured for what was asked.
            message:
              type: string
              description: A human readable summary. Do not branch on it, branch on `code`.
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      description: Provisioning cannot answer on this deployment right now.
    DeviceFulfilment:
      type: object
      properties:
        orderId:
          type: string
          description: >-
            Parent checkout order/group reference. Authenticated read only; it
            authorizes nothing. Older rows without this field use id.
        quantity:
          type: integer
          minimum: 1
          maximum: 3
          description: Phones purchased in this group.
        unitIndex:
          type: integer
          minimum: 1
          maximum: 3
          description: >-
            This independently fulfilled phone's one-based position in the
            group.
        id:
          type: string
          description: >-
            This purchase's identifier, stable across redeliveries of the same
            payment.
        state:
          type: string
          enum:
            - provisioned
            - awaiting_provisioning
            - cancelled
            - pool_access
          description: >-
            `provisioned` a device is bound to your organisation and appears on
            `/v1/devices`. `awaiting_provisioning` the purchase succeeded but no
            device was free at that moment; one is being prepared for you, and
            `estimatedBy` is when it is expected. `cancelled` the purchase was
            undone and any device it had provided was returned. `pool_access`
            the purchase buys the RIGHT to take a device out of the shared pool
            while you need one, rather than a device of your own: `deviceId` is
            null and stays null, nothing is being prepared, and nobody owes you
            hardware. Lease a device when you want one and you are billed for
            the minutes you hold it.
        deviceId:
          type:
            - string
            - 'null'
          description: >-
            The device this purchase provided, or null while it is still being
            prepared.
        estimatedBy:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            When provisioning is expected, and null unless `state` is
            `awaiting_provisioning`. It is the STORED date, not one recomputed
            on read: a date that moves forward each time you look can never be
            late.


            It CAN be moved, by us, and that is why it is called an estimate
            rather than a promise. The move is recorded with who made it. This
            field was called `promisedBy` until the estimate became movable;
            nothing consumed that spelling.
        sku:
          type:
            - string
            - 'null'
          description: >-
            What was bought, from the same set `GET /v1/skus` publishes. Null on
            a purchase made before the choice was recorded, which reads as 'we
            do not know' and never as a tier. It survives settlement and
            cancellation, because 'what did they buy' is asked most often about
            a purchase that is no longer live.
        billing:
          type:
            - string
            - 'null'
          description: >-
            How it is paid for, from the same set `GET /v1/skus` publishes. Null
            on a purchase that predates the field.
        initialTermVerified:
          type: boolean
          description: >-
            Inclusion follows the immutable purchase source: legacy and
            non-applicable orders omit this field; managed and unknown sources
            always include an explicit boolean. True requires an allocated order
            whose initial supplier term and first paid invoice commit are
            verified in the local ledger. False makes no such assertion and does
            not grant control. This historical signal proves neither current
            supplier availability nor current Stripe payment health; device
            control remains separately authorized.
        deliveryPhase:
          type: string
          enum:
            - preparing
            - waiting_for_provider
            - configuring
            - verifying_service
            - needs_attention
            - ready
          description: >-
            Safe delivery progress for Cloud orders. A needs_attention phase
            requires recovery; it is not active boot progress. Ready requires
            allocation and the managed initial service proof. Older responses
            may omit this field.
        model:
          type: object
          properties:
            id:
              type: string
              description: >-
                The model id the purchase recorded, as it was sent on `POST
                /v1/checkout-sessions`.
            name:
              type: string
              description: >-
                The name this deployment shows for that id TODAY, or the id
                itself when nothing declares it any more. A renamed card renames
                the purchase; a withdrawn model still names what was bought.
          required:
            - id
            - name
          additionalProperties: false
          description: >-
            Which model was chosen at purchase. ABSENT when none was chosen, and
            on every purchase made before models existed; both are fulfilled
            from any unit of the kind, so a reader treats absence as 'no
            particular model', never as unknown.
        region:
          type: string
          pattern: ^[A-Z]{2}$
          description: >-
            Which region was chosen at purchase, as the upper-case ISO 3166-1
            alpha-2 code sent on `POST /v1/checkout-sessions`. ABSENT when none
            was chosen, and on every purchase made before regions existed; both
            were placed in the model's default region, so a reader treats
            absence as 'the default', never as unknown. A code and never a name;
            name it yourself.
        revocationReason:
          type:
            - string
            - 'null'
          enum:
            - subscription_deleted
            - refunded
            - disputed
            - operator
            - customer_cancelled
            - customer_returned
            - expired
            - null
          description: >-
            Why this purchase stopped being live. Null while it IS live, which
            is the common case, AND on a purchase cancelled before this was
            recorded. So null on a `cancelled` row means 'not recorded', never a
            reason of its own, and a reader must not render it as one.
        purchasedAt:
          type: string
          format: date-time
          description: When the purchase settled.
        addOns:
          $ref: '#/components/schemas/CheckoutAddOns'
      required:
        - id
        - state
        - deviceId
        - estimatedBy
        - sku
        - billing
        - revocationReason
        - purchasedAt
      additionalProperties: false
      description: One purchase and what it turned into.
    CheckoutAddOns:
      type: object
      properties:
        usFixedIp:
          type: boolean
          description: >-
            Ask for a US fixed IP for this phone after delivery. Not charged
            with the phone: the console opens the existing managed-proxy quote
            and checkout once the phone is ready, a second charge.
        preinstallApps:
          type: boolean
          description: Ask to be shown the Apps upload page once the phone is ready. Free.
      required:
        - usFixedIp
        - preinstallApps
      additionalProperties: false
      description: >-
        Free add-on intents recorded on a checkout; neither is a payment line.
        On a `DeviceFulfilment` it is sent on every row from this release: both
        false when none was chosen or the purchase predates add-ons.
  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.