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

# Bind, unbind, transfer or replace a Proxy

> Requires a first-party interactive organization session with devices:read. Financial commands require an administrator and the configured checkout step-up policy. Network commands also require devices:act, current device authority and explicit consent. New purchase/connection commands require current admission. No credentials are returned. A deployment without the durable composition returns capability_unavailable (422). Renewal, order target/delivery edits, operation resume and event feeds are not available. Preserve command identity after an uncertain reply and read the original order or operation.

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



## OpenAPI

````yaml /api/openapi.json post /v1/proxy-operations
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/proxy-operations:
    post:
      tags:
        - Managed Proxy
      summary: Bind, unbind, transfer or replace a Proxy
      description: >-
        Requires a first-party interactive organization session with
        devices:read. Financial commands require an administrator and the
        configured checkout step-up policy. Network commands also require
        devices:act, current device authority and explicit consent. New
        purchase/connection commands require current admission. No credentials
        are returned. A deployment without the durable composition returns
        capability_unavailable (422). Renewal, order target/delivery edits,
        operation resume and event feeds are not available. Preserve command
        identity after an uncertain reply and read the original order or
        operation.


        Hosted deployments only. A local checkout does not mount this route, so
        calling it there is a 404.
      operationId: postV1ProxyOperations
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: >-
            Stable identity for this exact intent. Reuse it with the original
            body after an uncertain response.
          schema:
            type: string
            minLength: 16
            maxLength: 128
            pattern: ^[ -~]+$
      requestBody:
        description: >-
          Exact confirmation and optimistic resource/device versions. Bind,
          transfer and replace require device_expiry_consent matching the target
          resource original disclosure; unbind forbids it. Server records the
          authenticated member and transaction time.
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ManagedProxyNetworkCommandRequest'
      responses:
        '201':
          description: >-
            Original command receipt. Follow status_url; acceptance is not
            payment, delivery or network proof.
          headers:
            Cache-Control:
              description: Responses are never cached.
              schema:
                type: string
                const: no-store
            X-Request-Id:
              description: Sanitized request correlation.
              schema:
                type: string
            Location:
              description: Original order or operation status URL.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedProxyCommandReceipt'
        '400':
          description: >-
            Rejected or unavailable; inspect the structured error and retain
            original command identity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedProxyErrorResponse'
        '401':
          description: >-
            Rejected or unavailable; inspect the structured error and retain
            original command identity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedProxyErrorResponse'
        '403':
          description: >-
            Rejected or unavailable; inspect the structured error and retain
            original command identity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedProxyErrorResponse'
        '404':
          description: >-
            Rejected or unavailable; inspect the structured error and retain
            original command identity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedProxyErrorResponse'
        '409':
          description: >-
            Rejected or unavailable; inspect the structured error and retain
            original command identity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedProxyErrorResponse'
        '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'
        '422':
          description: >-
            Rejected or unavailable; inspect the structured error and retain
            original command identity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedProxyErrorResponse'
        '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: >-
            Rejected or unavailable; inspect the structured error and retain
            original command identity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedProxyErrorResponse'
      security:
        - bearerAuth: []
components:
  schemas:
    ManagedProxyNetworkCommandRequest:
      oneOf:
        - $ref: '#/components/schemas/ManagedProxyBindRequest'
        - $ref: '#/components/schemas/ManagedProxyUnbindRequest'
        - $ref: '#/components/schemas/ManagedProxyTransferRequest'
        - $ref: '#/components/schemas/ManagedProxyReplaceRequest'
      discriminator:
        propertyName: kind
        mapping:
          bind: '#/components/schemas/BindRequest'
          unbind: '#/components/schemas/UnbindRequest'
          transfer: '#/components/schemas/TransferRequest'
          replace: '#/components/schemas/ReplaceRequest'
    ManagedProxyCommandReceipt:
      type: object
      description: >-
        Local durable acceptance only. Replay returns the same command/target
        and original accepted version, not a new side effect.
      properties:
        command_id:
          $ref: '#/components/schemas/ManagedProxyId'
        target_type:
          type: string
          enum:
            - quote
            - order
            - proxy
            - operation
        target_id:
          $ref: '#/components/schemas/ManagedProxyId'
        accepted_version:
          $ref: '#/components/schemas/ManagedProxyVersion'
        status_url:
          type: string
          description: Relative URL of stable readable target.
          pattern: ^/v1/
        request_id:
          $ref: '#/components/schemas/ManagedProxyId'
      required:
        - command_id
        - target_type
        - target_id
        - accepted_version
        - status_url
        - request_id
      additionalProperties: false
    ManagedProxyErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Stable snake_case code.
              pattern: ^[a-z][a-z0-9_]*$
            message:
              type: string
              description: English, without credentials or raw vendor errors.
            request_id:
              $ref: '#/components/schemas/ManagedProxyId'
            details:
              $ref: '#/components/schemas/ManagedProxyErrorDetails'
            retryable:
              type: boolean
              description: Compatibility with existing reverification_required envelope.
            maxAgeMinutes:
              type: integer
              minimum: 1
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      examples:
        - error:
            code: version_conflict
            message: >-
              The device or resource changed. Refresh its status before
              confirming again.
            request_id: req_demo_2
            details:
              retryable: false
              recovery_action: refresh_and_confirm
              resource_versions:
                px_demo_1: 8
              device_versions:
                dev_demo_a: 13
                dev_demo_b: 4
        - error:
            code: reverification_required
            message: Verify again and retry.
            retryable: true
            maxAgeMinutes: 10
    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.
    ManagedProxyBindRequest:
      type: object
      properties:
        expected_version:
          $ref: '#/components/schemas/ManagedProxyVersion'
        resource_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        device_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        proxy_id:
          $ref: '#/components/schemas/ManagedProxyId'
        confirm_network_interruption:
          type: boolean
          const: true
        prior_operation_id:
          $ref: '#/components/schemas/ManagedProxyId'
        expected_prior_operation_version:
          $ref: '#/components/schemas/ManagedProxyVersion'
        kind:
          type: string
          const: bind
        target_device_id:
          $ref: '#/components/schemas/ManagedProxyId'
        device_expiry_consent:
          $ref: '#/components/schemas/ManagedProxyExpiryAcceptance'
      required:
        - expected_version
        - resource_versions
        - device_versions
        - proxy_id
        - confirm_network_interruption
        - kind
        - target_device_id
        - device_expiry_consent
      additionalProperties: false
      dependentRequired:
        prior_operation_id:
          - expected_prior_operation_version
        expected_prior_operation_version:
          - prior_operation_id
    ManagedProxyUnbindRequest:
      type: object
      properties:
        expected_version:
          $ref: '#/components/schemas/ManagedProxyVersion'
        resource_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        device_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        proxy_id:
          $ref: '#/components/schemas/ManagedProxyId'
        confirm_network_interruption:
          type: boolean
          const: true
        prior_operation_id:
          $ref: '#/components/schemas/ManagedProxyId'
        expected_prior_operation_version:
          $ref: '#/components/schemas/ManagedProxyVersion'
        kind:
          type: string
          const: unbind
        source_device_id:
          $ref: '#/components/schemas/ManagedProxyId'
        confirm_default_network:
          type: boolean
          const: true
      required:
        - expected_version
        - resource_versions
        - device_versions
        - proxy_id
        - confirm_network_interruption
        - kind
        - source_device_id
        - confirm_default_network
      additionalProperties: false
      dependentRequired:
        prior_operation_id:
          - expected_prior_operation_version
        expected_prior_operation_version:
          - prior_operation_id
      description: >-
        Explicit detach of an existing/unresolved binding. Expired or
        unavailable resource and changed purchase admission do not prohibit
        detach; actual device authority, consent and remote-conflict safety
        still apply.
    ManagedProxyTransferRequest:
      type: object
      properties:
        expected_version:
          $ref: '#/components/schemas/ManagedProxyVersion'
        resource_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        device_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        proxy_id:
          $ref: '#/components/schemas/ManagedProxyId'
        confirm_network_interruption:
          type: boolean
          const: true
        prior_operation_id:
          $ref: '#/components/schemas/ManagedProxyId'
        expected_prior_operation_version:
          $ref: '#/components/schemas/ManagedProxyVersion'
        kind:
          type: string
          const: transfer
        source_device_id:
          $ref: '#/components/schemas/ManagedProxyId'
        target_device_id:
          $ref: '#/components/schemas/ManagedProxyId'
        confirm_default_network:
          type: boolean
          const: true
        device_expiry_consent:
          $ref: '#/components/schemas/ManagedProxyExpiryAcceptance'
      required:
        - expected_version
        - resource_versions
        - device_versions
        - proxy_id
        - confirm_network_interruption
        - kind
        - source_device_id
        - target_device_id
        - confirm_default_network
        - device_expiry_consent
      additionalProperties: false
      dependentRequired:
        prior_operation_id:
          - expected_prior_operation_version
        expected_prior_operation_version:
          - prior_operation_id
    ManagedProxyReplaceRequest:
      type: object
      properties:
        expected_version:
          $ref: '#/components/schemas/ManagedProxyVersion'
        resource_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        device_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        proxy_id:
          $ref: '#/components/schemas/ManagedProxyId'
        confirm_network_interruption:
          type: boolean
          const: true
        prior_operation_id:
          $ref: '#/components/schemas/ManagedProxyId'
        expected_prior_operation_version:
          $ref: '#/components/schemas/ManagedProxyVersion'
        kind:
          type: string
          const: replace
        target_device_id:
          $ref: '#/components/schemas/ManagedProxyId'
        replacement_proxy_id:
          $ref: '#/components/schemas/ManagedProxyId'
        device_expiry_consent:
          $ref: '#/components/schemas/ManagedProxyExpiryAcceptance'
      required:
        - expected_version
        - resource_versions
        - device_versions
        - proxy_id
        - confirm_network_interruption
        - kind
        - target_device_id
        - replacement_proxy_id
        - device_expiry_consent
      additionalProperties: false
      dependentRequired:
        prior_operation_id:
          - expected_prior_operation_version
        expected_prior_operation_version:
          - prior_operation_id
    ManagedProxyId:
      type: string
      description: Opaque organization-bound identifier.
      minLength: 1
      maxLength: 128
      pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$
    ManagedProxyVersion:
      type: integer
      minimum: 1
    ManagedProxyErrorDetails:
      type: object
      properties:
        retryable:
          type: boolean
        recovery_action:
          type: string
        resource_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        device_versions:
          $ref: '#/components/schemas/ManagedProxyVersionMap'
        order_id:
          $ref: '#/components/schemas/ManagedProxyId'
        operation_id:
          $ref: '#/components/schemas/ManagedProxyId'
      required: []
      additionalProperties: false
    ManagedProxyVersionMap:
      type: object
      maxProperties: 16
      propertyNames:
        $ref: '#/components/schemas/ManagedProxyId'
      additionalProperties:
        $ref: '#/components/schemas/ManagedProxyVersion'
      description: >-
        Exact affected customer resource/device IDs and authoritative versions.
        Server validates completeness; hidden stock is guarded internally.
    ManagedProxyExpiryAcceptance:
      type: object
      properties:
        accepted:
          type: boolean
          const: true
        disclosure_version:
          type: string
          minLength: 1
          maxLength: 128
          pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$
        disclosure_digest:
          type: string
          pattern: ^[a-f0-9]{64}$
      required:
        - accepted
        - disclosure_version
        - disclosure_digest
      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.