> ## 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 what can be bought and what it costs

> The price list, one entry per pair of sku and billing mode, so that a caller drawing a purchase screen has a number to show before the customer commits. Prices are PRE TAX and come from the payment provider's own price objects; this service reports them and never computes one.

Any signed in principal in the organisation may read it, of either role, because a member who is allowed to buy has to be allowed to see what things cost. The admin rule lives on the purchase itself.

A pair this deployment holds no price for is ABSENT from the list rather than present without a price, so the list is exactly what can be bought here today. Buying a pair that is not in it answers 503.

Each entry says whether it ships today as `stock`, a verdict and never a count. The shelf is read once per request for the whole list, and a shelf that cannot be read makes every entry `unknown` rather than failing the list: prices that were read are still shown.

Nothing in the answer is specific to your organisation.

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/skus
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/skus:
    get:
      tags:
        - Provisioning
      summary: List what can be bought and what it costs
      description: >-
        The price list, one entry per pair of sku and billing mode, so that a
        caller drawing a purchase screen has a number to show before the
        customer commits. Prices are PRE TAX and come from the payment
        provider's own price objects; this service reports them and never
        computes one.


        Any signed in principal in the organisation may read it, of either role,
        because a member who is allowed to buy has to be allowed to see what
        things cost. The admin rule lives on the purchase itself.


        A pair this deployment holds no price for is ABSENT from the list rather
        than present without a price, so the list is exactly what can be bought
        here today. Buying a pair that is not in it answers 503.


        Each entry says whether it ships today as `stock`, a verdict and never a
        count. The shelf is read once per request for the whole list, and a
        shelf that cannot be read makes every entry `unknown` rather than
        failing the list: prices that were read are still shown.


        Nothing in the answer is specific to your organisation.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: getV1Skus
      responses:
        '200':
          description: What this deployment sells.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SkuList'
        '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 has no priced catalogue: no provider configured, or
            the provider did not answer. The same code the purchase route
            answers, because from a caller's side they are one situation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisioningUnavailableError'
      security:
        - bearerAuth: []
components:
  schemas:
    SkuList:
      type: object
      properties:
        skus:
          type: array
          items:
            $ref: '#/components/schemas/CatalogEntry'
          description: >-
            One entry per purchasable pair of sku and billing mode. A pair this
            deployment holds no price for is absent rather than present without
            a price: a card a caller cannot put a number on is one it cannot
            honestly offer. The robot arm skus are never listed with `payg`:
            that rate stays on the published rate card and is not on sale yet.
      required:
        - skus
      additionalProperties: false
    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.
    CatalogEntry:
      type: object
      properties:
        maxQuantity:
          type: integer
          minimum: 1
          maximum: 3
          description: >-
            Maximum phones of one exact selected model in a single payment.
            Three only for the enabled Cloud first-month payment plan; legacy
            plans support one. Absent on older servers means one.
        sku:
          type: string
          enum:
            - emulator
            - cloud_phone
            - phone_robot_android
            - phone_robot_ios
          description: What is being bought.
        billing:
          type: string
          enum:
            - subscription
            - payg
            - day
          description: >-
            How it is paid for. `payg` is charged by the minute. `day` is a
            one-off charge for a minimum of one day on a dedicated device; no
            sku lists it today (robot arm devices are sold monthly only), and it
            stays in the vocabulary for day passes bought earlier. A sku's
            `subscription` row always comes first.
        price:
          $ref: '#/components/schemas/SkuPrice'
        models:
          type: array
          items:
            $ref: '#/components/schemas/CatalogModel'
          minItems: 1
          description: >-
            The models a buyer may choose between for this pair, when it offers
            any. ABSENT when it offers none or when their stock has never been
            readable; never an empty array. With one model there is nothing to
            choose and a client may show it as a fact; with two or more, show a
            chooser. A purchase may name one of these ids as `model`, or name
            none and be fulfilled from any unit of the kind as before. Read this
            LAST and never let it fail the row: it is descriptive, and a row
            without it is still a price.
        shipsNow:
          type: boolean
          description: >-
            Whether buying this today gets a working device straight away. True
            exactly when `available` is at least one, which counts what is free
            in inventory AND what this deployment can still obtain. False
            therefore means it can do neither.


            False does NOT mean the purchase is refused. By default it succeeds
            and the device is promised instead, which `GET /v1/fulfilments` then
            reports as `awaiting_provisioning` with a deadline. It is a flag,
            not a quantity: read `available` for how many are free.


            True exactly when `stock` is `in_stock`. `stock` also says when the
            answer is not known, which this field cannot: read `stock` when that
            difference matters.
        stock:
          type: string
          enum:
            - in_stock
            - out_of_stock
            - unknown
          description: >-
            The stock verdict for the kind of unit this sku is fulfilled with,
            from the same rule the purchase route uses when this deployment is
            configured to refuse a sale with nothing on the shelf (`409
            out_of_stock` on `POST /v1/checkout-sessions`). A verdict;
            `available` beside it is the count.


            `in_stock`: this deployment can deliver one, whether from inventory
            or by obtaining it. `out_of_stock`: it can do neither. By default
            the purchase still succeeds and the device is promised, which `GET
            /v1/fulfilments` reports as `awaiting_provisioning` with a deadline.
            `unknown`: this deployment has no inventory to ask, or asking it
            failed just now. The row is still priced and still buyable; say you
            cannot tell how soon rather than guessing either way.


            The same for every caller in every organisation: it describes the
            shelf, not your account.
        available:
          type: integer
          minimum: 0
          description: >-
            How many devices of this sku this deployment could deliver.


            TWO TERMS: units already free in inventory, plus how many more it
            may still obtain. For a cloud phone those are the same product and
            arrive the same way, so the number is the total. A sku that can only
            come out of existing inventory reports just that.


            ABSENT, never `0`, when `stock` is `unknown`: zero is an answer and
            this deployment does not have one. So a present `available` is a
            fact and an absent one means `cannot say`, without having to read
            `stock` to tell which kind of zero you are looking at.


            IT IS NOT A RESERVATION. Nothing is held for you by reading it, so
            three purchases against `available: 1` can all be made and two of
            them will be promised a device rather than handed one. Use it to
            show what is obtainable and to explain a disabled button, not to
            plan a batch.


            The same for every caller in every organisation: it describes this
            deployment, not your account.
        delivery:
          type: string
          enum:
            - now
            - about_24h
            - waitlist
          description: >-
            How soon a purchase gets a working device.


            `now`: `available` is at least one. `about_24h`: nothing is free
            right now, and a spare unit can be made ready in about a day; the
            purchase is promised, with its deadline on `GET /v1/fulfilments`.
            `waitlist`: neither.


            `shipsNow` is exactly `delivery == now`. ABSENT exactly when
            `available` is: when inventory could not be read there is no lead
            time to report.
      required:
        - sku
        - billing
        - price
        - shipsNow
        - stock
      additionalProperties: false
    SkuPrice:
      oneOf:
        - $ref: '#/components/schemas/FlatSkuPrice'
        - $ref: '#/components/schemas/MeteredSkuPrice'
      discriminator:
        propertyName: kind
        mapping:
          flat: '#/components/schemas/FlatSkuPrice'
          metered: '#/components/schemas/MeteredSkuPrice'
      description: >-
        What one purchasable pair costs. Read `kind` FIRST, and do not infer it
        from the amount: a metered rate can be a fraction of a minor unit, but
        it can equally be a whole one, so the number tells you nothing about
        which variant you are holding. `flat` is one amount per `period`.
        `metered` is `amountMinorDecimal` per `unit`, invoiced every `period`.
        Those last two are different questions and both have to be read: two
        cents per MINUTE, invoiced every MONTH.
    CatalogModel:
      type: object
      properties:
        id:
          type: string
          description: >-
            The value to send back as `model` on `POST /v1/checkout-sessions`,
            and the value a purchase records. A lowercase slug, stable across
            renames of `name`.
        name:
          type: string
          description: >-
            What to show the buyer: a handset or product name. Free to change
            without moving a purchase.
        androidVersion:
          type: string
          description: >-
            The Android version this model runs, as a free-form string such as
            `13`. Descriptive only.
        spec:
          type: string
          description: >-
            The spec line the card shows, joined with ` · `, such as `12G RAM ·
            Google CTS · 8 cores`. Absent when the model has none. Descriptive
            only.
        popular:
          type: boolean
          description: >-
            True when the model belongs under the Popular tab. Absent means
            false. Display only.
        brand:
          type: string
          description: >-
            The handset's maker, such as `Google` or `Samsung`, for grouping and
            filtering. Absent on a generic tier, which has no maker to name.
        kind:
          type: string
          enum:
            - standard
            - branded
          description: >-
            Which group to draw this model in: `standard` is a generic Android
            tier, `branded` is a named handset and carries a `brand`. Sent
            explicitly so a reader never has to infer the grouping from whether
            `brand` is present.
        price:
          $ref: '#/components/schemas/SkuPrice'
        quoteId:
          type: string
          description: >-
            The current provider Price id for this model, used only as an opaque
            quote identity. Send it as `quoteId` with a selected model at
            checkout. The server resolves the charge from its own mapping and
            returns 409 `quote_changed` if the quote no longer matches. Both
            price and quoteId are absent when this model has no configured
            price.
        shipsNow:
          type: boolean
          description: >-
            Whether this model can be bought and handed over today. With
            `regions` present it means in at least one of them; read the
            region's own flag for a particular one. False is still listed so the
            buyer can see the model exists; use `backorderable` for a sold-out
            default that can be purchased. Per MODEL, and independent of the
            row's own `shipsNow`, which is about units already on the shelf.
        defaultShipsNow:
          type: boolean
          description: >-
            Whether this model can ship now in the operator's default region
            when `region` is omitted. Present when the model declares a region
            axis; absent when it has none. `shipsNow` may be true while this is
            false, because another allowed region has stock. A client hiding
            region choice should use this field and `backorderable` instead of
            silently picking another country.
        backorderable:
          type: boolean
          description: >-
            True only when the last successful supplier goods listing resolved
            this model and its configured default region (if any), the no-region
            checkout path is sold out, and the model has a valid exact monthly
            `price` and `quoteId`. The buyer may pay now with no `region` field;
            provisioning waits for that exact model and landing region. False
            for unlisted goods or regions and for missing prices. Optional for
            compatibility; treat absence as false. Checkout rechecks.
        regions:
          type: array
          items:
            $ref: '#/components/schemas/CatalogRegion'
          minItems: 1
          description: >-
            The regions a buyer may place this model in, in the order to draw
            them, each with its own stock answer. ABSENT when the model has no
            region axis, and never an empty array: then draw no region chooser,
            send no `region`, and the deployment's default region for the model
            applies. Stock is per model per region, so a model can ship in one
            region and not another; price does not vary by region.
      required:
        - id
        - name
        - kind
        - shipsNow
      additionalProperties: false
      description: >-
        One model a buyer may choose for a pair. Read `id` to send and `name` to
        show; never derive one from the other.
    FlatSkuPrice:
      type: object
      properties:
        kind:
          type: string
          enum:
            - flat
          description: One whole amount per billing period.
        amountMinor:
          type: integer
          description: >-
            The price before tax, in the MINOR unit of `currency`: 1900 is
            nineteen dollars when the currency is `usd`. Named for the unit
            because a field called `amount` invites exactly one wrong reading
            and it is the expensive one. This is the payment provider's own
            number for the price object, carried through unaltered; this service
            does no arithmetic on a charge.
        currency:
          type: string
          description: >-
            The provider's currency code for this price, lower case, as it
            states it.
        period:
          $ref: '#/components/schemas/PricePeriod'
      required:
        - kind
        - amountMinor
        - currency
      additionalProperties: false
    MeteredSkuPrice:
      type: object
      properties:
        kind:
          type: string
          enum:
            - metered
          description: An amount per unit of measured usage.
        amountMinorDecimal:
          type: string
          pattern: ^[0-9]+(\.[0-9]+)?$
          description: >-
            The price of ONE `unit`, before tax, in the MINOR unit of
            `currency`, as a decimal STRING: `"2"` with a currency of `usd` is
            two cents, which is $0.02, and `"0.5"` is half a cent. A string and
            not a number because the rate need not be a whole minor unit, and
            binary floating point is the wrong container for money. It is the
            provider's own value, carried through unaltered; this service does
            no arithmetic on a charge and does not convert this field. A
            whole-numbered value here is still a metered rate: read `kind`,
            never the shape of the number.
        currency:
          type: string
          description: >-
            The provider's currency code for this price, lower case, as it
            states it.
        unit:
          type: string
          enum:
            - minute
          description: >-
            What one unit of the measured quantity is. `minute` is a wall-clock
            minute of device time, rounded up, minimum one per lease.
        period:
          $ref: '#/components/schemas/PricePeriod'
      required:
        - kind
        - amountMinorDecimal
        - currency
        - unit
        - period
      additionalProperties: false
    CatalogRegion:
      type: object
      properties:
        code:
          type: string
          pattern: ^[A-Z]{2}$
          description: >-
            The region as an upper-case ISO 3166-1 alpha-2 code (`US`, `JP`,
            `GB`), and the value to send back as `region`. A code and never a
            name: name it yourself, for instance with `Intl.DisplayNames`.
        shipsNow:
          type: boolean
          description: >-
            Whether this model can be placed in this region today. False is
            still listed so the buyer can see the region exists. A listed
            sold-out region may be purchased as a backorder; Checkout rechecks
            its listing.
      required:
        - code
        - shipsNow
      additionalProperties: false
      description: One region a buyer may place a model in, with its own stock answer.
    PricePeriod:
      type: object
      properties:
        interval:
          type: string
          enum:
            - day
            - week
            - month
            - year
          description: The unit of the billing cycle.
        count:
          type: integer
          minimum: 1
          description: >-
            How many `interval`s make one billing cycle. 1 with an `interval` of
            `month` is monthly; 3 would be quarterly. Stated rather than
            assumed, because assuming 1 would print a quarterly price as a
            monthly one.
      required:
        - interval
        - count
      additionalProperties: false
  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.