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

# Create line webhook

> ⚠️ **Real side effect — this is NOT a simulation, including from the playground.** Creates a phone-line webhook and answers **`201`** with `{ "webhook": { … } }`, including **`secret`** — the `plwh_…` signing secret. **This is the only time it is ever returned**: store it right away (the response is sent with `Cache-Control: no-store`). There is no rotation: to change it, create a new webhook, move your receiver to its secret, and delete the old one.

Not idempotent: two calls create two webhooks, each with its own secret. A workspace can have more than one phone-line webhook; each receives the events it subscribed to. Events are never replayed to a webhook created after them.

Permissions: with an OAuth / MCP token, the user must be an **Owner or Admin** (`phone_lines:webhooks`) — `403 PERMISSION_DENIED` otherwise.

Every `/v1/phone-lines` endpoint takes a **tenant-scoped** key only: phone-line webhooks belong 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`.

These are the **phone-line webhooks** — a registry separate from `/v1/webhooks`: they receive only `phone_line.*` events, and a number webhook never receives one.

**Requires a tenant-scoped key.**

> [!WARNING]
>
> **This creates a real webhook**, and events start going to its URL right away. The response carries the `plwh_` signing secret **only this once** — copy it from the response before leaving the page: nothing shows it again. See [Configure](https://docs.pilotstatus.com.br/api/phone-lines/webhooks.md#configure).


## OpenAPI

````yaml openapi.json POST /v1/phone-lines/webhooks
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/webhooks:
    post:
      tags:
        - Phone Lines
      summary: Create a phone-line webhook
      description: >-
        ⚠️ **Real side effect — this is NOT a simulation, including from the
        playground.** Creates a phone-line webhook and answers **`201`** with `{
        "webhook": { … } }`, including **`secret`** — the `plwh_…` signing
        secret. **This is the only time it is ever returned**: store it right
        away (the response is sent with `Cache-Control: no-store`). There is no
        rotation: to change it, create a new webhook, move your receiver to its
        secret, and delete the old one.


        Not idempotent: two calls create two webhooks, each with its own secret.
        A workspace can have more than one phone-line webhook; each receives the
        events it subscribed to. Events are never replayed to a webhook created
        after them.


        Permissions: with an OAuth / MCP token, the user must be an **Owner or
        Admin** (`phone_lines:webhooks`) — `403 PERMISSION_DENIED` otherwise.


        Every `/v1/phone-lines` endpoint takes a **tenant-scoped** key only:
        phone-line webhooks belong 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`.


        These are the **phone-line webhooks** — a registry separate from
        `/v1/webhooks`: they receive only `phone_line.*` events, and a number
        webhook never receives one.


        **Requires a tenant-scoped key.**
      operationId: post_phone_lines_webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
                - events
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 100
                  description: >-
                    Optional, up to 100 characters (`400 INVALID_WEBHOOK_NAME`
                    above that). Blank or absent → the URL's host.
                  example: Phone lines — production
                url:
                  type: string
                  description: >-
                    Must be `https` and reach a public address. A URL that
                    points to an internal or private network address —
                    `localhost`, a loopback or private IP, a host without a dot,
                    or a name that resolves to one — → `400
                    WEBHOOK_URL_REFUSED`.
                  example: https://hooks.example.com/pilot/phone-lines
                events:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum:
                      - phone_line.purchased
                      - phone_line.audio_received
                      - phone_line.code_received
                      - phone_line.code_failed
                      - phone_line.renewed
                      - phone_line.payment_failed
                      - phone_line.suspended
                      - phone_line.returned
                      - phone_line.canceled
                      - phone_line.verification_updated
                      - '*'
                  description: >-
                    One or more of the phone-line events, or `"*"` for all of
                    them (inside this registry `"*"` means every phone-line
                    event, including any added later). Duplicates are removed.
                    An empty list or an unknown event → `400 INVALID_WEBHOOK`.
                  example:
                    - '*'
            example:
              url: https://hooks.example.com/pilot/phone-lines
              events:
                - '*'
      responses:
        '201':
          description: Created — `secret` is returned only this once
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhook:
                    type: object
                    properties:
                      id:
                        type: string
                        description: >-
                          The webhook id. Use it in every
                          `/v1/phone-lines/webhooks/{id}` path.
                        example: cmh1w8b2k0001wh8example3d
                      name:
                        type: string
                        description: >-
                          Up to 100 characters. **Never empty:** created or
                          edited blank, it is the URL's host
                          (`hooks.example.com`). Not sent in deliveries — the
                          `name` of a delivery is the line's.
                        example: hooks.example.com
                      url:
                        type: string
                        description: The `https` destination, normalized.
                        example: https://hooks.example.com/pilot/phone-lines
                      events:
                        type: array
                        items:
                          type: string
                        description: >-
                          The subscribed events, or `["*"]` — every phone-line
                          event, including any added later.
                        example:
                          - '*'
                      active:
                        type: boolean
                        description: '`false` = paused: receives no deliveries.'
                        example: true
                      secret:
                        type: string
                        description: >-
                          The signing secret (`plwh_…`). **Returned only here,
                          once** — no endpoint shows it again and there is no
                          rotation. It is the HMAC-SHA256 key of
                          `x-pilot-status-signature`.
                        example: plwh_3f9c1a7e5b2d4c6e8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d
                      createdAt:
                        type: string
                        format: date-time
                        description: ISO 8601 — when the webhook was created.
                        example: '2026-10-27T13:58:02.000Z'
              example:
                webhook:
                  id: cmh1w8b2k0001wh8example3d
                  name: hooks.example.com
                  url: https://hooks.example.com/pilot/phone-lines
                  events:
                    - '*'
                  active: true
                  secret: plwh_3f9c1a7e5b2d4c6e8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d
                  createdAt: '2026-10-27T13:58:02.000Z'
          headers:
            Cache-Control:
              schema:
                type: string
              example: no-store
        '400':
          description: >-
            Invalid request: body not a JSON object (`INVALID_BODY`), fields
            other than `name`, `url` and `events` (`UNKNOWN_FIELDS`,
            `details.unknownFields`), a field with the wrong JSON type
            (`INVALID_FIELD_TYPE`, `details.field` / `details.expected`), a URL
            that is not `https` or an empty / unknown event list
            (`INVALID_WEBHOOK`), a `name` over 100 characters
            (`INVALID_WEBHOOK_NAME`), or a URL that points to an internal or
            private network address (`WEBHOOK_URL_REFUSED`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
                  details:
                    type: object
              examples:
                invalidBody:
                  summary: Body is not a JSON object
                  value:
                    error: >-
                      Send a JSON object: { "url": "https://example.com/hook",
                      "events": ["phone_line.code_received"] }. | Envie um
                      objeto JSON: { "url": "https://example.com/hook",
                      "events": ["phone_line.code_received"] }.
                    code: INVALID_BODY
                unknownFields:
                  summary: Unknown field
                  value:
                    error: >-
                      Unknown field(s): secret. Accepted: name, url, events. |
                      Campo(s) desconhecido(s): secret. Aceitos: name, url,
                      events.
                    code: UNKNOWN_FIELDS
                    details:
                      unknownFields:
                        - secret
                invalidFieldType:
                  summary: Field with the wrong type
                  value:
                    error: >-
                      "events" must be an array of strings. | "events" deve ser
                      uma lista de textos.
                    code: INVALID_FIELD_TYPE
                    details:
                      field: events
                      expected: string[]
                invalidWebhook:
                  summary: Not https, or empty / unknown events
                  value:
                    error: >-
                      Invalid webhook: use an https URL and known events. |
                      Webhook inválido: use uma URL https e eventos conhecidos.
                    code: INVALID_WEBHOOK
                invalidName:
                  summary: Name over 100 characters
                  value:
                    error: >-
                      Invalid webhook name: use at most 100 characters. | Nome
                      do webhook inválido: use no máximo 100 caracteres.
                    code: INVALID_WEBHOOK_NAME
                urlRefused:
                  summary: Internal or private destination
                  value:
                    error: >-
                      Webhook URL refused: it points to an internal or private
                      network address, which we never call. Use a public
                      address. | URL do webhook recusada: ela aponta para um
                      endereço interno ou de rede privada, que nunca chamamos.
                      Use um endereço público.
                    code: WEBHOOK_URL_REFUSED
        '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: >-
            Number-scoped key (`NUMBER_SCOPE_NOT_ALLOWED`), OAuth/MCP token of a
            user who is not Owner or Admin (`PERMISSION_DENIED`) or no longer an
            active member (`WORKSPACE_MEMBERSHIP_REQUIRED`), or archived
            workspace (`WORKSPACE_ARCHIVED`). `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 Owner or Admin
                  value:
                    error: >-
                      Your workspace role does not allow this action
                      (phone_lines:webhooks).
                    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
                tenantRequired:
                  summary: Credential not bound to a workspace
                  value:
                    error: This credential is not bound to a workspace.
                    code: TENANT_REQUIRED
        '500':
          description: Unexpected failure (`INTERNAL_ERROR`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string
              example:
                error: Unexpected error. | Erro inesperado.
                code: INTERNAL_ERROR
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.