> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pilotstatus.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Buy phone lines

> ⚠️ **Real side effect: this call charges money — it is NOT a simulation, including from the playground.** Each number costs the full monthly price (R$ 33,90 for BRL workspaces, US$ 33.90 for USD), taken from your **wallet credits first** and then from your **saved card**, and becomes a line of your workspace, `ACTIVE` and paid for its first month. There is no proration and no checkout link: with no credits and no saved card the item fails with `PAYMENT_FAILED`.

Buys **1 to 10** numbers taken from `GET /v1/phone-lines/available`. The numbers are **independent**: each one is charged and acquired on its own, in the order sent, and one that fails does **not** undo the others. So the request answers **`200`** when it was processed, with one result per number — **a `200` does not mean you bought anything**; always read `results[i].ok`. A failed item carries `code` and a bilingual `error`, but no HTTP status of its own:

| Item `code` | What happened | Charged? |
| --- | --- | --- |
| `REQUEST_ALREADY_PROCESSED` | This `Idempotency-Key` was already used for this number within the last hour — whatever that attempt's outcome was. | Not by this request. |
| `NUMBER_UNAVAILABLE` | Already held by a line (any workspace, yours included), being bought in another request right now, or the carrier no longer has it. | No — or refunded to the wallet. |
| `PAYMENT_FAILED` | Credits plus saved card did not cover the price, or the card charge did not go through. | No — credits taken were put back. |
| `PAYMENT_PROVIDER_ERROR` | The card processor did not answer. The card **may** have been charged. | Wallet: no. Card: check your statement. |
| `PURCHASE_NOT_COMPLETED` | Charged and acquired, but the line could not be recorded. | Refunded to the wallet. |
| `SUPPLIER_UNAVAILABLE` | The carrier failed while acquiring the number. | Refunded to the wallet. |

A refund after a charge always goes to the **wallet**, including any part paid by card.

Anything other than `200` means **no number was attempted**, with one exception: a `500` (or a timeout) can come after earlier numbers were already charged and bought — retry the **same request with the same `Idempotency-Key`**, then list your lines with `GET /v1/phone-lines`.

**Idempotency (required header).** The key is tracked **per number, for one hour**: the same key with the same number charges nothing and answers `ok: false`, `REQUEST_ALREADY_PROCESSED` — whether the first attempt bought the number or failed (a replay does not return the original result). To retry a failed number, use a **new** key. A request refused as a whole (`400`/`403`) records nothing, so it can be resent with the same key. The key is scoped to your workspace. If the idempotency cache is unavailable the protection is skipped rather than blocking purchases.

**Identity verification first.** The account holder must be verified (through `/v1/phone-lines/verification` or the dashboard): never started or unfinished → `403 VERIFICATION_REQUIRED`; rejected → `403 VERIFICATION_REJECTED`; under manual review → proceeds, but lines bought during the review are returned and refunded to the wallet if it is rejected.

Permissions: with an OAuth / MCP token, only the workspace **Owner** may buy (`phone_lines:purchase`) — `403 PERMISSION_DENIED` otherwise.

Every `/v1/phone-lines` endpoint takes a **tenant-scoped** key only: a line belongs to the workspace, not to a WhatsApp number, so a number-scoped key (or a per-number OAuth grant) gets `403 NUMBER_SCOPE_NOT_ALLOWED` before any role is looked at. A `403 PERMISSION_DENIED` / `WORKSPACE_MEMBERSHIP_REQUIRED` is sent with `Cache-Control: no-store`.

**Requires a tenant-scoped key.**

> [!WARNING]
>
> **This is a real purchase.** Each number costs the full monthly price, taken from your wallet credits first and then from your saved card. Use a new `Idempotency-Key` for each purchase, and repeat a key only to retry that same purchase. See [Buy phone lines](https://docs.pilotstatus.com.br/api/phone-lines/purchase.md).


## OpenAPI

````yaml openapi.json POST /v1/phone-lines
openapi: 3.1.0
info:
  title: Pilot Status API
  version: 1.0.0
  license:
    name: Pilot Status Terms of Service
    url: https://pilotstatus.com.br/terms
  description: >-
    Public REST API for Pilot Status. Authenticate with the `x-api-key: ps_...`
    header (or `x-api-key-id`). Base URL: https://pilotstatus.com.br
servers:
  - url: https://pilotstatus.com.br
security:
  - apiKey: []
  - apiKeyId: []
paths:
  /v1/phone-lines:
    post:
      tags:
        - Phone Lines
      summary: Buy phone lines
      description: >-
        ⚠️ **Real side effect: this call charges money — it is NOT a simulation,
        including from the playground.** Each number costs the full monthly
        price (R$ 33,90 for BRL workspaces, US$ 33.90 for USD), taken from your
        **wallet credits first** and then from your **saved card**, and becomes
        a line of your workspace, `ACTIVE` and paid for its first month. There
        is no proration and no checkout link: with no credits and no saved card
        the item fails with `PAYMENT_FAILED`.


        Buys **1 to 10** numbers taken from `GET /v1/phone-lines/available`. The
        numbers are **independent**: each one is charged and acquired on its
        own, in the order sent, and one that fails does **not** undo the others.
        So the request answers **`200`** when it was processed, with one result
        per number — **a `200` does not mean you bought anything**; always read
        `results[i].ok`. A failed item carries `code` and a bilingual `error`,
        but no HTTP status of its own:


        | Item `code` | What happened | Charged? |

        | --- | --- | --- |

        | `REQUEST_ALREADY_PROCESSED` | This `Idempotency-Key` was already used
        for this number within the last hour — whatever that attempt's outcome
        was. | Not by this request. |

        | `NUMBER_UNAVAILABLE` | Already held by a line (any workspace, yours
        included), being bought in another request right now, or the carrier no
        longer has it. | No — or refunded to the wallet. |

        | `PAYMENT_FAILED` | Credits plus saved card did not cover the price, or
        the card charge did not go through. | No — credits taken were put back.
        |

        | `PAYMENT_PROVIDER_ERROR` | The card processor did not answer. The card
        **may** have been charged. | Wallet: no. Card: check your statement. |

        | `PURCHASE_NOT_COMPLETED` | Charged and acquired, but the line could
        not be recorded. | Refunded to the wallet. |

        | `SUPPLIER_UNAVAILABLE` | The carrier failed while acquiring the
        number. | Refunded to the wallet. |


        A refund after a charge always goes to the **wallet**, including any
        part paid by card.


        Anything other than `200` means **no number was attempted**, with one
        exception: a `500` (or a timeout) can come after earlier numbers were
        already charged and bought — retry the **same request with the same
        `Idempotency-Key`**, then list your lines with `GET /v1/phone-lines`.


        **Idempotency (required header).** The key is tracked **per number, for
        one hour**: the same key with the same number charges nothing and
        answers `ok: false`, `REQUEST_ALREADY_PROCESSED` — whether the first
        attempt bought the number or failed (a replay does not return the
        original result). To retry a failed number, use a **new** key. A request
        refused as a whole (`400`/`403`) records nothing, so it can be resent
        with the same key. The key is scoped to your workspace. If the
        idempotency cache is unavailable the protection is skipped rather than
        blocking purchases.


        **Identity verification first.** The account holder must be verified
        (through `/v1/phone-lines/verification` or the dashboard): never started
        or unfinished → `403 VERIFICATION_REQUIRED`; rejected → `403
        VERIFICATION_REJECTED`; under manual review → proceeds, but lines bought
        during the review are returned and refunded to the wallet if it is
        rejected.


        Permissions: with an OAuth / MCP token, only the workspace **Owner** may
        buy (`phone_lines:purchase`) — `403 PERMISSION_DENIED` otherwise.


        Every `/v1/phone-lines` endpoint takes a **tenant-scoped** key only: a
        line belongs to the workspace, not to a WhatsApp number, so a
        number-scoped key (or a per-number OAuth grant) gets `403
        NUMBER_SCOPE_NOT_ALLOWED` before any role is looked at. A `403
        PERMISSION_DENIED` / `WORKSPACE_MEMBERSHIP_REQUIRED` is sent with
        `Cache-Control: no-store`.


        **Requires a tenant-scoped key.**
      operationId: post_phone_lines
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: >-
            **Required.** Any string up to **255 characters** with no control
            characters — a UUID per purchase is the usual choice. Tracked per
            number for one hour, scoped to your workspace: a retry with the same
            key never charges twice for the same number (the item answers
            `REQUEST_ALREADY_PROCESSED`). Missing, empty or whitespace-only →
            **400 `IDEMPOTENCY_KEY_REQUIRED`**; too long or with a control
            character (tab included) → **400 `IDEMPOTENCY_KEY_INVALID`**. It is
            a header, never a body field.
          schema:
            type: string
            minLength: 1
            maxLength: 255
          example: 6f1c2a0e-7b1d-4c2e-9a5f-3d8e1b2c4a70
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - numbers
              additionalProperties: false
              properties:
                numbers:
                  type: array
                  minItems: 1
                  maxItems: 10
                  uniqueItems: true
                  items:
                    type: string
                    pattern: ^55[1-9][0-9][0-9]{8}$
                  description: >-
                    1 to 10 distinct numbers, each exactly as `GET
                    /v1/phone-lines/available` returned it in `number`: E.164
                    digits **without** `+` — `55`, a two-digit area code
                    starting 1–9, then 8 digits (`551148637200`). Numbers from
                    different area codes can go in the same request. `numbers`
                    is the **only** accepted field: anything else is refused
                    with `400 UNKNOWN_FIELDS` (the dashboard's `requestId` does
                    not exist here).
                  example:
                    - '551148637200'
                    - '551148637201'
            example:
              numbers:
                - '551148637200'
                - '551148637201'
      responses:
        '200':
          description: >-
            Processed: one result per number, in the order sent. Read
            `results[i].ok` — every item may have failed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    description: One entry per number, in the order sent.
                    items:
                      type: object
                      required:
                        - number
                        - ok
                      properties:
                        number:
                          type: string
                          description: The number, as you sent it.
                          example: '551148637200'
                        ok:
                          type: boolean
                          description: '`true` when the line was bought by this request.'
                          example: true
                        lineId:
                          type: string
                          description: >-
                            Only when `ok: true`. The new line's `id`, for `GET
                            /v1/phone-lines/{id}` and the activation-code
                            endpoints.
                          example: cmg5k2l1n0001pl8example9x
                        code:
                          type: string
                          description: >-
                            Only when `ok: false`. Why this number failed:
                            `REQUEST_ALREADY_PROCESSED`, `NUMBER_UNAVAILABLE`,
                            `PAYMENT_FAILED`, `PAYMENT_PROVIDER_ERROR`,
                            `PURCHASE_NOT_COMPLETED` or `SUPPLIER_UNAVAILABLE`.
                          enum:
                            - REQUEST_ALREADY_PROCESSED
                            - NUMBER_UNAVAILABLE
                            - PAYMENT_FAILED
                            - PAYMENT_PROVIDER_ERROR
                            - PURCHASE_NOT_COMPLETED
                            - SUPPLIER_UNAVAILABLE
                          example: PAYMENT_FAILED
                        error:
                          type: string
                          description: >-
                            Only when `ok: false`. Bilingual message (`English |
                            Portuguese`) for that `code`.
                          example: >-
                            The payment did not go through. Add balance or check
                            your card. | O pagamento não foi concluído. Adicione
                            saldo ou confira seu cartão.
              examples:
                mixed:
                  summary: One bought, one payment failure
                  value:
                    results:
                      - number: '551148637200'
                        ok: true
                        lineId: cmg5k2l1n0001pl8example9x
                      - number: '551148637201'
                        ok: false
                        code: PAYMENT_FAILED
                        error: >-
                          The payment did not go through. Add balance or check
                          your card. | O pagamento não foi concluído. Adicione
                          saldo ou confira seu cartão.
                replay:
                  summary: Replay with the same key
                  value:
                    results:
                      - number: '551148637200'
                        ok: false
                        code: REQUEST_ALREADY_PROCESSED
                        error: >-
                          This request was already processed. Check your lines,
                          or send a new request id (Idempotency-Key in the API)
                          to try again. | Este pedido já foi processado. Confira
                          suas linhas, ou envie um novo id de pedido
                          (Idempotency-Key na API) para tentar de novo.
                      - number: '551148637201'
                        ok: false
                        code: REQUEST_ALREADY_PROCESSED
                        error: >-
                          This request was already processed. Check your lines,
                          or send a new request id (Idempotency-Key in the API)
                          to try again. | Este pedido já foi processado. Confira
                          suas linhas, ou envie um novo id de pedido
                          (Idempotency-Key na API) para tentar de novo.
        '400':
          description: >-
            Request refused as a whole — nothing was attempted or charged.
            Validation messages are bilingual and may carry `details`
            (`maxLength`, `unknownFields`, `invalidNumbers`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
                  details:
                    type: object
              examples:
                idempotencyKeyRequired:
                  summary: No Idempotency-Key
                  value:
                    error: >-
                      Send an Idempotency-Key header (for example a UUID) so a
                      retry cannot buy twice. | Envie o header Idempotency-Key
                      (por exemplo um UUID) para que uma nova tentativa não
                      compre duas vezes.
                    code: IDEMPOTENCY_KEY_REQUIRED
                idempotencyKeyInvalid:
                  summary: Idempotency-Key too long / control character
                  value:
                    error: >-
                      Idempotency-Key must have at most 255 characters and no
                      control characters. | Idempotency-Key deve ter no máximo
                      255 caracteres e nenhum caractere de controle.
                    code: IDEMPOTENCY_KEY_INVALID
                    details:
                      maxLength: 255
                invalidBody:
                  summary: Body is not a JSON object
                  value:
                    error: >-
                      Send a JSON object: { "numbers": ["551148637200"] }. |
                      Envie um objeto JSON: { "numbers": ["551148637200"] }.
                    code: INVALID_BODY
                unknownFields:
                  summary: Field other than numbers
                  value:
                    error: >-
                      Unknown field(s): requestId. This endpoint accepts only
                      "numbers"; the idempotency key goes in the Idempotency-Key
                      header. | Campo(s) desconhecido(s): requestId. Este
                      endpoint aceita apenas "numbers"; a chave de idempotência
                      vai no header Idempotency-Key.
                    code: UNKNOWN_FIELDS
                    details:
                      unknownFields:
                        - requestId
                numbersNotArray:
                  summary: numbers is not an array of strings
                  value:
                    error: >-
                      "numbers" must be an array of strings. | "numbers" deve
                      ser uma lista de textos.
                    code: INVALID_NUMBERS
                invalidNumbers:
                  summary: Entries not shaped like a searched number
                  value:
                    error: >-
                      Not a number from GET /v1/phone-lines/available:
                      +551148637201, 11948637202. Use E.164 digits without "+",
                      e.g. 551148637200. | Não é um número de GET
                      /v1/phone-lines/available: +551148637201, 11948637202. Use
                      os dígitos E.164 sem "+", ex.: 551148637200.
                    code: INVALID_NUMBERS
                    details:
                      invalidNumbers:
                        - '+551148637201'
                        - '11948637202'
                invalidSelection:
                  summary: '>10, zero, or duplicated numbers'
                  value:
                    error: >-
                      Pick between 1 and 10 numbers from the search. | Escolha
                      entre 1 e 10 números da busca.
                    code: INVALID_SELECTION
        '401':
          description: >-
            Missing or invalid credential (`x-api-key`, `x-api-key-id` or
            `Authorization: Bearer`). The body carries only `error`, no `code`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
              examples:
                missing:
                  summary: No credential
                  value:
                    error: >-
                      Missing x-api-key, x-api-key-id or Authorization Bearer
                      token
                invalid:
                  summary: Invalid API key
                  value:
                    error: Invalid API key
        '403':
          description: >-
            Refused as a whole, nothing attempted: wrong key scope, role,
            archived workspace, or account holder not verified / rejected.
            `TENANT_REQUIRED`: credential not bound to any workspace
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
              examples:
                numberScope:
                  summary: Number-scoped key
                  value:
                    error: This endpoint requires a tenant-scoped API key
                    code: NUMBER_SCOPE_NOT_ALLOWED
                permissionDenied:
                  summary: OAuth/MCP user who is not the Owner
                  value:
                    error: >-
                      Your workspace role does not allow this action
                      (phone_lines:purchase).
                    code: PERMISSION_DENIED
                notMember:
                  summary: OAuth/MCP user no longer a member
                  value:
                    error: >-
                      The user behind this token is not an active member of this
                      workspace.
                    code: WORKSPACE_MEMBERSHIP_REQUIRED
                archived:
                  summary: Workspace archived
                  value:
                    error: >-
                      This workspace has been archived and its API is closed. |
                      Este workspace foi arquivado e sua API está encerrada.
                    code: WORKSPACE_ARCHIVED
                verificationRequired:
                  summary: Holder not verified
                  value:
                    error: >-
                      Verify the account holder before buying a line. |
                      Verifique o titular da conta antes de comprar uma linha.
                    code: VERIFICATION_REQUIRED
                verificationRejected:
                  summary: Verification rejected
                  value:
                    error: >-
                      The account holder verification was rejected. | A
                      verificação do titular foi recusada.
                    code: VERIFICATION_REJECTED
                tenantRequired:
                  summary: Credential not bound to a workspace
                  value:
                    error: This credential is not bound to a workspace.
                    code: TENANT_REQUIRED
        '404':
          description: >-
            An `x-whatsapp-number-id` header was sent and names no WhatsApp
            number of your account (`NUMBER_NOT_FOUND`). This endpoint needs no
            such header — remove it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
              example:
                error: >-
                  x-whatsapp-number-id does not name a WhatsApp number of this
                  account | x-whatsapp-number-id não indica um número de
                  WhatsApp desta conta
                code: NUMBER_NOT_FOUND
        '500':
          description: >-
            Unexpected failure (`INTERNAL_ERROR`). **Does not prove that nothing
            happened**: earlier numbers may already have been charged and
            bought. Retry the same request with the **same** `Idempotency-Key`,
            then list your lines.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
              example:
                error: Unexpected error. | Erro inesperado.
                code: INTERNAL_ERROR
        '503':
          description: >-
            The carrier integration is unavailable before any number is
            attempted (`SUPPLIER_UNAVAILABLE`). Nothing was charged. (Carrier
            failures during a purchase come back per item, inside a `200`.)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
              example:
                error: >-
                  Phone lines are temporarily unavailable. Try again in a few
                  minutes. | As linhas estão temporariamente indisponíveis.
                  Tente de novo em alguns minutos.
                code: SUPPLIER_UNAVAILABLE
components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: Your ps_ API key
    apiKeyId:
      type: apiKey
      in: header
      name: x-api-key-id
      description: API key id (alternative to x-api-key)

````

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