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

# Enroll a gateway computer

> A new gateway computer trades an enrollment code, once, for its gateway id and tunnel credential. The installer calls it over HTTPS before the computer has any credential, so no `Authorization` is read: the code in the body is the credential. A gateway-bound code has 8 characters and lives 15 minutes; a legacy site-only code has 6 characters and lives 30 minutes. Both are used once and belong to one site. Both shapes are accepted during the rollout overlap.

A code that cannot be used is always answered the same way (410 `ENROLL_CODE_INVALID`, the same message), whether it is malformed, unknown, expired, revoked, locked or already used. The response body does not reveal whether a code exists; this is not a constant-time guarantee. `ENROLL_CODE_EXPIRED` is retired.

Failures are throttled before any database work, per api process: 10 a minute per client (an IPv4 address, or an IPv6 /64), and 60 a minute with a well-formed code across all clients. Once the global budget is spent, sources with recent successful enrollment or tunnel authentication share one extra 20-per-minute lane; their per-client limit still applies. Malformed codes spend only the per-client budget. Successful exchanges and 503 responses refund both reservations. A request carrying a real code with another field wrong counts against that code; after 5 the code is refused for good. At most 4 exchanges are in progress at once per machine; beyond that the answer is 503 with `Retry-After: 1`, not counted as a failure. Bodies over 8192 bytes are read as carrying no code.

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



## OpenAPI

````yaml /api/openapi.json post /gateway/enroll
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:
  /gateway/enroll:
    post:
      tags:
        - Gateway enrollment
      summary: Enroll a gateway computer
      description: >-
        A new gateway computer trades an enrollment code, once, for its gateway
        id and tunnel credential. The installer calls it over HTTPS before the
        computer has any credential, so no `Authorization` is read: the code in
        the body is the credential. A gateway-bound code has 8 characters and
        lives 15 minutes; a legacy site-only code has 6 characters and lives 30
        minutes. Both are used once and belong to one site. Both shapes are
        accepted during the rollout overlap.


        A code that cannot be used is always answered the same way (410
        `ENROLL_CODE_INVALID`, the same message), whether it is malformed,
        unknown, expired, revoked, locked or already used. The response body
        does not reveal whether a code exists; this is not a constant-time
        guarantee. `ENROLL_CODE_EXPIRED` is retired.


        Failures are throttled before any database work, per api process: 10 a
        minute per client (an IPv4 address, or an IPv6 /64), and 60 a minute
        with a well-formed code across all clients. Once the global budget is
        spent, sources with recent successful enrollment or tunnel
        authentication share one extra 20-per-minute lane; their per-client
        limit still applies. Malformed codes spend only the per-client budget.
        Successful exchanges and 503 responses refund both reservations. A
        request carrying a real code with another field wrong counts against
        that code; after 5 the code is refused for good. At most 4 exchanges are
        in progress at once per machine; beyond that the answer is 503 with
        `Retry-After: 1`, not counted as a failure. Bodies over 8192 bytes are
        read as carrying no code.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: postGatewayEnroll
      requestBody:
        description: What the computer says about itself, and the code.
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnrollRequest'
      responses:
        '201':
          description: 'Enrolled. `Cache-Control: no-store`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Enrolled'
        '400':
          description: '`ENROLL_REQUEST_INVALID`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrollError'
        '410':
          description: >-
            `ENROLL_CODE_INVALID`, the one answer for a code that cannot be
            used.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrollError'
        '413':
          description: >-
            The request body exceeds the server's limit (1 MiB). Refused before
            the body is parsed, so nothing was partially applied. Retrying the
            same body will fail the same way: send less. Per-field limits are
            tighter than this and are documented on the fields themselves; this
            is the backstop underneath them. The shape is the same on ordinary
            JSON surfaces, /vrx included. The explicitly configured APK content
            stream documents its own byte ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayloadTooLargeError'
        '429':
          description: >-
            `ENROLL_RATE_LIMITED`: too many failures from this client, or from
            everyone. Wait, then retry with the same code.


            Or: 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: Seconds until a retry can be decided.
              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:
                oneOf:
                  - $ref: '#/components/schemas/EnrollError'
                  - $ref: '#/components/schemas/RateLimitedError'
        '503':
          description: >-
            `SERVICE_UNAVAILABLE`: no database on this deployment, or it failed,
            or the machine is busy (then with `Retry-After: 1`). Not counted as
            a failure.
          headers:
            Retry-After:
              description: Seconds to wait; present when the machine was busy.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrollError'
      security: []
components:
  schemas:
    EnrollRequest:
      type: object
      properties:
        code:
          type: string
          description: >-
            The enrollment code: 8 Crockford Base32 characters for a
            gateway-bound code, or 6 for a legacy site-only code during the
            overlap. Case, spaces and hyphens are forgiven, and O, I and L are
            read as 0, 1 and 1. Other lengths are refused.
        publicKey:
          type: string
          description: >-
            The Ed25519 public key generated on the computer: 32 bytes, unpadded
            base64url (43 characters; one trailing `=` is forgiven). A
            non-canonical point encoding or one of the eight points of small
            order is refused with ENROLL_REQUEST_INVALID. The computer keeps the
            private key in its secret store and signs its continuity proofs with
            it; the platform keeps only this public key and verifies the proofs
            against it.
        host:
          type: object
          properties:
            os:
              type: string
              enum:
                - darwin
                - linux
                - win32
              description: The computer's operating system.
            osVersion:
              type: string
              description: At most 64 characters.
            arch:
              type: string
              enum:
                - arm64
                - x64
              description: The computer's processor architecture.
            hostname:
              type: string
              description: >-
                At most 255 characters. Becomes a legacy gateway's display name;
                a gateway-bound code preserves the issuer's display name.
            machineIdHash:
              type: string
              description: >-
                sha256 of the platform machine id, 64 hex characters. Only used
                to recognise a reinstall.
          required:
            - os
            - osVersion
            - arch
            - hostname
            - machineIdHash
          additionalProperties: false
        edge:
          type: object
          properties:
            version:
              type: string
              description: At most 64 characters.
            gitSha:
              type: string
              description: 7 to 40 hex characters.
          required:
            - version
            - gitSha
          additionalProperties: false
      required:
        - code
        - publicKey
        - host
        - edge
      additionalProperties: false
    Enrolled:
      type: object
      properties:
        gatewayId:
          type: string
          description: >-
            The gateway's id: the pre-created target for a gateway-bound code,
            or a new id for a legacy site-only code.
        credential:
          type: string
          description: >-
            The gateway's tunnel credential, in this response only. It serves no
            device until one is bound to the gateway. Keep it in the platform
            secret store, never in shell history, environment files or service
            units.
        tunnelUrl:
          type: string
          description: The WebSocket URL the gateway connects to with its credential.
        releaseChannel:
          type: string
          description: >-
            The gateway's release channel: `production` on the declared
            production deployment; otherwise the legacy default `stable`.
        siteId:
          type: string
          description: The site the code was issued for.
      required:
        - gatewayId
        - credential
        - tunnelUrl
        - releaseChannel
        - siteId
      additionalProperties: false
    EnrollError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - ENROLL_CODE_INVALID
                - ENROLL_REQUEST_INVALID
                - ENROLL_RATE_LIMITED
                - SERVICE_UNAVAILABLE
              description: >-
                `ENROLL_CODE_INVALID` (410): the code cannot be used, whether it
                is malformed, unknown, expired, used, revoked or refused after
                too many failures. The answer is the same in every case.
                `ENROLL_REQUEST_INVALID` (400): the code is well formed and
                another field is not; the message names it.
                `ENROLL_RATE_LIMITED` (429). `SERVICE_UNAVAILABLE` (503): retry
                with the same code. `ENROLL_CODE_EXPIRED` is retired and no
                longer sent: an expired code is answered as
                `ENROLL_CODE_INVALID`. A client that still lists it loses
                nothing.
            message:
              type: string
              description: >-
                A sentence for the person setting the computer up. Do not branch
                on it, branch on `code`.
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
    PayloadTooLargeError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              const: payload_too_large
              description: Always `payload_too_large` on this response.
            message:
              type: string
              description: A human readable summary naming the limit in bytes.
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      description: >-
        Returned when the request body exceeds the server limit. Identical on
        every surface, including /vrx.
    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.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.