openapi: 3.1.0
info:
  title: DialStack API
  version: 1.0.0
  description: |
    DialStack — Business Voice for Vertical SaaS

    ## Authentication
    DialStack supports three authentication methods:

    ### Platform API Keys (Backend)
    For server-side integrations, use your secret API key (starts with `sk_live_`) in the Authorization header.

    Example: `Authorization: Bearer sk_live_YOUR_SECRET_KEY`

    ### Session Tokens (Embedded Components)
    For embedded voice components in your frontend, create a session using the Account Session API
    and use the client_secret JWT token in the Authorization header.

    Example: `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...`

    Sessions are account-scoped and expire after 1 hour.

    ### User Tokens (Softphones & User-Facing Apps)
    For client-side applications where end users interact directly (softphones, call history,
    voicemail), authenticate users via `POST /v1/user_sessions` and use the returned user token.

    Example: `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...`

    User tokens are scoped to a single user within an account and grant access to the WebRTC
    signalling channel and user-scoped REST endpoints such as emergency addresses. See the
    [Authentication guide](/guides/authentication#user-tokens) for setup details.

    ## Account Context
    Most API endpoints require an account context. This is specified differently depending on the authentication method:

    ### Platform API Keys
    Include the `DialStack-Account` header with the account ID:
    ```
    DialStack-Account: acct_01h2xcejqtf2nbrexx3vqjhp41
    ```

    ### Session Tokens
    The account is automatically derived from the JWT claims. No header is needed.

    ## Pagination
    All list API methods support URL-based cursor pagination. Results are returned in reverse chronological order.

    ### Request Parameters
    - `limit` - Number of objects to return (1-100, default 10)
    - `page` - Opaque cursor token (only used when following pagination URLs)

    ### Response Structure
    List responses include:
    - `object` - Always "list"
    - `url` - The endpoint URL
    - `next_page_url` - URL to fetch the next page (null if no more pages)
    - `previous_page_url` - URL to fetch the previous page (null if on first page)
    - `data` - Array of objects

    ### Usage
    To paginate, simply follow the URLs provided in `next_page_url` and `previous_page_url`.
    Do not construct pagination URLs manually - the cursor format is opaque and may change.

    ## Expanding Responses
    Some endpoints support the `expand[]` parameter to include related resources inline.
    Without expansion, the related data is omitted from the response.

    For example, to include extensions when listing users:
    ```
    GET /v1/users?expand[]=extensions
    ```

    The expanded field appears as a nested list object:
    ```json
    {
      "id": "user_01h2xcejqtf2nbrexx3vqjhp42",
      "name": "Dr. Alice Smith",
      "extensions": {
        "object": "list",
        "data": [
          {
            "number": "105",
            "target": "user_01h2xcejqtf2nbrexx3vqjhp42",
            "status": "active",
            "created_at": "2025-10-18T10:00:00Z",
            "updated_at": "2025-10-18T10:00:00Z"
          }
        ]
      }
    }
    ```

    Expandable fields are noted in endpoint descriptions.

    ## Base URL
    - `https://api.dialstack.ai`

    ## Voice Apps
    For building programmable voice applications with webhooks and real-time audio streaming,
    see the [Voice Apps Guide](/guides/voice-apps).

    ## Routing Targets
    A **routing target** is a resource that can receive calls. The following resource
    types are valid routing targets:
    - **User** — rings all of the user's registered devices (desk phones, softphones)
    - **Voice App** — delivers the call via webhook notification for programmatic control or monitoring
    - **Dial Plan** — routes the call through a dial plan graph
    - **Ring Group** — rings all group members simultaneously
    - **Shared Voicemail** — sends the call directly to a shared voicemail box

    ## Resource IDs
    All resource IDs in the API are opaque strings of at most 255 characters.
    Do not parse or make assumptions about their format — treat them as identifiers
    to store and pass back to the API.

  contact:
    name: DialStack API Support
    email: info@dialstack.ai
    url: https://docs.dialstack.ai/api
  license:
    name: Proprietary
    url: https://dialstack.ai/terms

servers:
  - url: https://api.dialstack.ai

security:
  - BearerAuth: []
  - SessionAuth: []

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_live_*
      description: |
        Platform secret API key (starts with sk_live_) for server-side integrations.
        Include in Authorization header:
        `Authorization: Bearer sk_live_YOUR_SECRET_KEY`

        For account-scoped operations, also include the `DialStack-Account` header.

    SessionAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Account session JWT token for embedded components.
        Create a session using POST /v1/account_sessions
        and use the client_secret JWT in the Authorization header:
        `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...`

        Sessions are account-scoped and expire after 1 hour.
        The account context is automatically derived from the JWT claims.

        Note: Session tokens cannot be used to create new sessions.

    UserAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        User token for softphones and user-facing applications.
        Obtain a token via POST /v1/user_sessions and include it in the Authorization header:
        `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...`

        User tokens are scoped to a single user within an account.
        They grant access to the WebRTC signalling channel and user-scoped
        REST endpoints (/v1/me/*). They cannot access platform-level endpoints.

        Tokens expire after the TTL requested at mint time (default 24 hours).
        To extend access, mint a new session via POST /v1/user_sessions.

  parameters:
    DialStackAccount:
      name: DialStack-Account
      in: header
      required: true
      description: |
        Account identifier for API key authentication.
        Not required when using session tokens (account is derived from JWT).
      schema:
        type: string
        example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'

    DialStackAccountOptional:
      name: DialStack-Account
      in: header
      required: false
      description: |
        Optional account identifier. When omitted, the operation applies to
        platform-global resources; when supplied, it applies to that account's
        resources. Used by endpoints that are platform-global by default but can
        be scoped to a single account (e.g. webhook endpoints).
      schema:
        type: string
        example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'

    AccountId:
      name: account_id
      in: path
      required: true
      description: Account identifier
      schema:
        type: string
        example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'

    UserId:
      name: user_id
      in: path
      required: true
      description: User identifier
      schema:
        type: string
        example: 'user_01h2xcejqtf2nbrexx3vqjhp42'

    VoicemailId:
      name: voicemail_id
      in: path
      required: true
      description: Voicemail identifier
      schema:
        type: string
        example: 'vm_01h2xcejqtf2nbrexx3vqjhp44'

    VoiceAppId:
      name: voice_app_id
      in: path
      required: true
      description: Voice app identifier
      schema:
        type: string
        example: 'va_01h2xcejqtf2nbrexx3vqjhp49'

    AIAgentId:
      name: ai_agent_id
      in: path
      required: true
      description: AI agent identifier
      schema:
        type: string
        example: 'aia_01h2xcejqtf2nbrexx3vqjhp60'

    ScheduleId:
      name: schedule_id
      in: path
      required: true
      description: Schedule identifier
      schema:
        type: string
        example: 'sched_01h2xcejqtf2nbrexx3vqjhp50'

    RingGroupId:
      name: ring_group_id
      in: path
      required: true
      description: Ring group identifier
      schema:
        type: string
        example: 'rg_01h2xcejqtf2nbrexx3vqjhp51'

    RingGroupMemberId:
      name: member_id
      in: path
      required: true
      description: Ring group member identifier
      schema:
        type: string
        example: 'rgm_01h2xcejqtf2nbrexx3vqjhp52'

    QueueId:
      name: queue_id
      in: path
      required: true
      description: Call queue identifier
      schema:
        type: string
        example: 'qu_01h2xcejqtf2nbrexx3vqjhp61'

    QueueMemberId:
      name: member_id
      in: path
      required: true
      description: Queue member identifier
      schema:
        type: string
        example: 'qum_01h2xcejqtf2nbrexx3vqjhp62'

    SharedVoicemailBoxId:
      name: shared_voicemail_box_id
      in: path
      required: true
      description: Shared voicemail box identifier
      schema:
        type: string
        example: 'svm_01h2xcejqtf2nbrexx3vqjhp60'

    AudioClipId:
      name: audio_clip_id
      in: path
      required: true
      description: Audio clip identifier
      schema:
        type: string

    FaxId:
      name: fax
      in: path
      required: true
      description: Fax identifier
      schema:
        type: string
        example: 'fax_01h2xcejqtf2nbrexx3vqjhp60'

    ExtensionNumber:
      name: number
      in: path
      required: true
      description: Extension number (dial code)
      schema:
        type: string
        example: '105'

    ExtensionTargetFilter:
      name: target
      in: query
      required: false
      description: Filter extensions by routing target ID
      schema:
        type: string
        maxLength: 255
        example: 'user_01h2xcejqtf2nbrexx3vqjhp42'

    CallId:
      name: call_id
      in: path
      required: true
      description: Call log identifier
      schema:
        type: string
        example: 'call_01h2xcejqtf2nbrexx3vqjhp45'

    ListenerId:
      name: listener_id
      in: path
      required: true
      description: Listener identifier
      schema:
        type: string
        example: 'lstn_01h2xcejqtf2nbrexx3vqjhp50'

    OrderId:
      name: order_id
      in: path
      required: true
      description: Phone number order identifier
      schema:
        type: string
        example: 'nord_01h2xcejqtf2nbrexx3vqjhp47'

    PortOrderId:
      name: order_id
      in: path
      required: true
      description: Port order identifier
      schema:
        type: string
        example: 'por_01h2xcejqtf2nbrexx3vqjhp53'

    PhoneNumberId:
      name: phone_number_id
      in: path
      required: true
      description: Phone number identifier
      schema:
        type: string
        example: 'did_01h2xcejqtf2nbrexx3vqjhp46'

    PhoneNumberStatus:
      name: status
      in: query
      required: false
      description: Filter by phone number status
      schema:
        type: string
        enum: [active, inactive, pending, released]
        example: 'active'

    NumberOrderStatus:
      name: status
      in: query
      required: false
      description: Filter by order status
      schema:
        type: string
        enum: [pending, complete, partial, failed]

    NumberOrderType:
      name: order_type
      in: query
      required: false
      description: Filter by order type
      schema:
        type: string
        enum: [order, disconnect]

    LocationId:
      name: location_id
      in: path
      required: true
      description: Location identifier
      schema:
        type: string
        example: 'loc_01h2xcejqtf2nbrexx3vqjhp50'

    LocationStatus:
      name: status
      in: query
      required: false
      deprecated: true
      description: |
        Deprecated. The operational-status field was retired; the filter is
        accepted for backwards compatibility but no longer narrows results.
      schema:
        type: string
        enum: [active, inactive]
        example: 'active'

    DeviceLineId:
      name: line_id
      in: path
      required: true
      description: Device line identifier
      schema:
        type: string
        example: 'dln_01h2xcejqtf2nbrexx3vqjhp55'

    ButtonTemplateId:
      name: id
      in: path
      required: true
      description: Button template identifier
      schema:
        type: string
        example: 'btpl_01h2xcejqtf2nbrexx3vqjhp70'

    ButtonId:
      name: button_id
      in: path
      required: true
      description: Template button identifier
      schema:
        type: string
        example: 'btn_01h2xcejqtf2nbrexx3vqjhp71'

    ButtonOverrideId:
      name: override_id
      in: path
      required: true
      description: Device button override identifier
      schema:
        type: string
        example: 'devbtn_01h2xcejqtf2nbrexx3vqjhp72'

    DeviceTypeFilter:
      name: type
      in: query
      required: false
      description: Filter devices by type
      schema:
        type: string
        enum: [deskphone, dect_base, dect_handset]

    PortOrderStatus:
      name: status
      in: query
      required: false
      description: Filter by port order status
      schema:
        type: string
        enum: [draft, approved, submitted, exception, foc, complete, cancelled]

    # Pagination Parameters
    Limit:
      name: limit
      in: query
      required: false
      description: |
        Number of objects to return.
        Defaults to 10. Maximum is 100.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 10
        example: 10

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    Forbidden:
      description: Account does not belong to this platform
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    AccountNotFound:
      description: Account not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    UserNotFound:
      description: User not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    VoicemailNotFound:
      description: Voicemail not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    CallNotFound:
      description: Call log not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    ListenerNotFound:
      description: Listener not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    CallNotActive:
      description: Call is not in a state that supports listeners (e.g., still ringing, already ended)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    TranscriptNotFound:
      description: Transcript not found (no recording exists for this call)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    RecordingNotFound:
      description: Recording not found (no recording exists for this call)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    VoiceAppNotFound:
      description: Voice app not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    AccountDeleted:
      description: Account deleted successfully

    UserDeleted:
      description: User deleted successfully

    VoicemailDeleted:
      description: Voicemail deleted successfully

    VoiceAppDeleted:
      description: Voice app deleted successfully

    AIAgentNotFound:
      description: AI agent not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    AIAgentDeleted:
      description: AI agent deleted successfully

    ScheduleNotFound:
      description: Schedule not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    ScheduleDeleted:
      description: Schedule deleted successfully

    DialPlanNotFound:
      description: Dial plan not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    DialPlanDeleted:
      description: Dial plan deleted successfully

    RoutingLoop:
      description: |
        Validation error — routing loop, AI-agent voice app referenced from a
        `notify`-mode `voice_app` node, or other dial-plan constraint violation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: 'routing loop detected: rg_01h2x... → dp_01h2x... → rg_01h2x...'

    ExtensionNotFound:
      description: Extension not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    ExtensionDeleted:
      description: Extension deleted successfully

    ExtensionConflict:
      description: Extension number already exists in this account
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    DialPlanConflict:
      description: Dial plan cannot be deleted because it is referenced by an extension
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    RingGroupNotFound:
      description: Ring group not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    RingGroupMemberNotFound:
      description: Ring group member not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    RingGroupDeleted:
      description: Ring group deleted successfully

    RingGroupMemberDeleted:
      description: Ring group member removed successfully

    RingGroupConflict:
      description: Ring group cannot be deleted because it is referenced by an extension
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    QueueNotFound:
      description: Queue not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    QueueMemberNotFound:
      description: Queue member not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    QueueDeleted:
      description: Queue deleted successfully

    QueueMemberDeleted:
      description: Queue member removed successfully

    QueueMemberConflict:
      description: User is already a member of this queue
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    QueueValidationFailed:
      description: |
        Request body is well-formed but references state that prevents the change.
        Examples:
        - Timeout target not found in this account.
        - Adding the queue would form a routing loop through ring groups, dial plans,
          or other queues.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    AudioClipNotFound:
      description: Audio clip not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    AudioClipDeleted:
      description: Audio clip deleted successfully

    FaxNotFound:
      description: Fax not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    FaxDeleted:
      description: Fax deleted successfully

    SharedVoicemailBoxNotFound:
      description: Shared voicemail box not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    SharedVoicemailBoxDeleted:
      description: Shared voicemail box deleted successfully

    SharedVoicemailBoxConflict:
      description: Shared voicemail box cannot be deleted because it is referenced by an extension
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    PhoneNumberNotFound:
      description: Phone number not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    NumberOrderNotFound:
      description: Number order not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    LocationNotFound:
      description: Location not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    PortOrderNotFound:
      description: Port order not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    LocationDeleted:
      description: Location deleted successfully

    DeviceNotFound:
      description: Device not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    DeviceLineNotFound:
      description: Device line not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    ButtonTemplateNotFound:
      description: Button template not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    ButtonOverrideNotFound:
      description: Button override not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    DeviceDeleted:
      description: Device deleted successfully

    DeviceLineDeleted:
      description: Device line deleted successfully

    DocumentNotFound:
      description: Document not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    HardwareOrderNotFound:
      description: Hardware order not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    Conflict:
      description: Conflict with current state of the resource
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    BadGateway:
      description: Upstream provider error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    # Reusable Property Definitions

    # --- WebRTC ---

    IceServersResponse:
      type: object
      required:
        - ice_servers
        - expires_at
      properties:
        ice_servers:
          type: array
          description: List of ICE server configurations for WebRTC peer connections
          items:
            $ref: '#/components/schemas/IceServer'
        expires_at:
          type: string
          format: date-time
          description: When the TURN credentials expire. Fetch new credentials before this time.

    IceServer:
      type: object
      required:
        - urls
      properties:
        urls:
          type: array
          items:
            type: string
          description: STUN or TURN server URL(s). Always an array, even for a single URL, matching `RTCIceServer.urls` in the W3C WebRTC spec.
          examples:
            - ['stun:stun.dialstack.ai:3478']
            - ['turn:turn.dialstack.ai:443?transport=tcp']
        username:
          type: string
          description: TURN username (not present for STUN servers)
        credential:
          type: string
          description: TURN credential (not present for STUN servers)

    WebPushKeyResponse:
      type: object
      required:
        - vapid_public_key
      properties:
        vapid_public_key:
          type: string
          description: |
            The public key the browser subscribes with, as base64url. Pass it to
            `pushManager.subscribe` as `applicationServerKey`.

    CreatePushSubscriptionRequest:
      type: object
      required:
        - endpoint
        - keys
      properties:
        endpoint:
          type: string
          description: |
            The browser Web Push destination URL (the `endpoint` of a browser
            `PushSubscription`). This is where the wake notification is sent.
        keys:
          type: object
          required:
            - p256dh
            - auth
          description: The subscription's encryption keys, used to encrypt the wake payload end-to-end to the browser.
          properties:
            p256dh:
              type: string
              description: The subscription's P-256 ECDH public key (base64url).
            auth:
              type: string
              description: The subscription's authentication secret (base64url).

    PushSubscription:
      type: object
      required:
        - id
        - endpoint
        - created_at
      properties:
        id:
          type: string
          description: Push subscription identifier
        endpoint:
          type: string
          description: The browser Web Push destination URL this subscription wakes.
        created_at:
          type: string
          format: date-time
          description: When the subscription was registered.

    # --- User-Scoped Resources ---

    UserProfile:
      type: object
      properties:
        id:
          type: string
          description: User identifier
        name:
          type: string
          description: User's display name
        email:
          type: string
          format: email
          description: User's email address
        account:
          type: string
          description: Account the user belongs to
        extensions:
          type: array
          description: User's assigned extension dial codes
          items:
            type: object
            properties:
              number:
                type: string
                description: Extension dial code
              status:
                type: string
                enum: [active, inactive]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    PresenceStatus:
      type: object
      required:
        - status
      properties:
        user:
          type: string
          description: User identifier
        status:
          type: string
          enum: [available, on_call, dnd, away, offline]
          description: |
            Current presence status:
            - `available` — user is online and can receive calls
            - `on_call` — user is currently on an active call
            - `dnd` — user has enabled Do Not Disturb
            - `away` — user is away (idle timeout or manual)
            - `offline` — no active WebRTC or SIP registration
        status_text:
          type: ['string', 'null']
          description: Optional custom status message
        updated_at:
          type: string
          format: date-time
          description: When the presence status last changed

    PresenceUpdateRequest:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum: [available, dnd, away]
          description: Desired presence status (cannot set `on_call` or `offline` manually)
        status_text:
          type: ['string', 'null']
          description: Optional custom status message (max 100 characters)

    DirectoryEntry:
      type: object
      description: |
        One member of the account directory, projected to only the fields a
        directory listing needs — never a full user object.
      required:
        - user
        - display_name
      properties:
        user:
          type: string
          description: Identifier of a colleague in the caller's account.
        display_name:
          type: string
          description: |
            The member's display name, suitable for a contact-list label. Falls
            back to the user identifier when the member has no name set.

    EmergencyAddress:
      type: object
      description: A registered emergency (E911) address resource.
      required:
        - id
        - address
        - registered_ip
        - created_at
      properties:
        id:
          type: string
          description: Unique identifier for the emergency address.
          examples:
            - 'emerg_01h2xcejqtf2nbrexx3vqjhp42'
        address:
          $ref: '#/components/schemas/EmergencyAddressDetails'
        registered_ip:
          type: ['string', 'null']
          description: |
            The network this address is bound to, or null when it has not yet
            been registered from a softphone session. Set automatically when a
            softphone presents this address on connect; cleared with
            `DELETE /v1/me/emergency-addresses/{emergency_address_id}/registered_ip`.
        created_at:
          type: string
          format: date-time

    EmergencyAddressDetails:
      type: object
      description: Normalized civic address fields.
      required:
        - city
        - state
        - postal_code
        - country
      properties:
        address_number:
          type: string
          description: House or building number.
          examples:
            - '123'
        street:
          type: string
          examples:
            - 'Main St'
        unit:
          type: string
          description: Suite, floor, apartment, or room number.
          examples:
            - 'Suite 400'
        city:
          type: string
          examples:
            - 'San Francisco'
        state:
          type: string
          description: Two-letter state/province code.
          examples:
            - 'CA'
        postal_code:
          type: string
          examples:
            - '94105'
        country:
          type: string
          description: Two-letter country code (ISO 3166-1 alpha-2).
          examples:
            - 'US'
        formatted_address:
          type: string
          description: Human-readable single-line address.
        latitude:
          type: number
        longitude:
          type: number

    EmergencyAddressRequest:
      type: object
      description: Civic address to register. Validated before it is stored.
      required:
        - street
        - city
        - state
        - postal_code
      properties:
        address_number:
          type: string
          description: House or building number.
        street:
          type: string
        unit:
          type: string
          description: Suite, floor, apartment, or room number.
        city:
          type: string
        state:
          type: string
          description: Two-letter state/province code.
        postal_code:
          type: string
        country:
          type: string
          description: Two-letter country code (ISO 3166-1 alpha-2). Defaults to US.

    HardwareCatalogItem:
      type: object
      properties:
        id:
          type: string
          description: Hardware catalog item ID
        manufacturer:
          type: string
          description: Hardware manufacturer
          example: SNOM
        model:
          type: string
          description: Product model name
          example: M500
        sku:
          type: ['string', 'null']
          description: Stock-keeping unit identifier
        device_type:
          type: string
          enum: [deskphone, dect_base, dect_handset, accessory]
          description: >-
            Type of item. `accessory` is a non-provisioned add-on (e.g. a power
            supply) that ships with an order but never becomes a device.
        active:
          type: boolean
          description: Whether the item is available for selection
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    HardwareOrder:
      type: object
      properties:
        id:
          type: string
          description: Hardware order ID
        status:
          type: string
          enum: [draft, submitted, approved, rejected, fulfilled, cancelled]
          description: >-
            Order lifecycle status. Orders are created as `submitted`, become
            `approved` after review, and `fulfilled` once delivered.
            `rejected` (declined at review) and `cancelled` are terminal.
        rejection_reason:
          type: ['string', 'null']
          description: >-
            Reason the order was declined at review. Only set when status is
            `rejected`.
        items:
          type: array
          description: Line items in the order
          items:
            $ref: '#/components/schemas/HardwareOrderItem'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    HardwareOrderItem:
      type: object
      description: >-
        A single physical unit on a hardware order. Requests speak quantity;
        responses expose one item per unit — aggregate client-side for
        display.
      properties:
        id:
          type: string
          description: Hardware order item ID
        user:
          type: ['string', 'null']
          description: >-
            Pre-assignment intent — the user this unit is destined for.
            Null when unassigned.
        location:
          type: ['string', 'null']
          description: >-
            Pre-staging intent — the physical location this unit is destined
            for. Null when not set.
        base_item:
          type: ['string', 'null']
          description: >-
            Pre-pairing intent (handsets only) — the ordered base unit this
            handset will pair with. Auto-set when the order has exactly one
            base; null otherwise.
        device:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - $ref: '#/components/schemas/Device'
            - type: 'null'
          readOnly: true
          description: |
            The device this unit materialized into at fulfillment. By default
            this is the device's id; when `expand[]=items.device` is requested
            it is the full device object, which carries its own `type`
            (`deskphone`, `dect_base`, `dect_handset`) and identity
            (`mac_address` / `ipei`). Null until the unit is fulfilled.
        fulfilled_at:
          type: ['string', 'null']
          format: date-time
          readOnly: true
          description: When this unit was fulfilled into a device. Null until fulfilled.
        hardware_catalog:
          $ref: '#/components/schemas/HardwareCatalogItem'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    HardwareOrderRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          items:
            type: object
            required: [hardware_catalog, quantity]
            properties:
              hardware_catalog:
                type: string
                description: ID of the hardware catalog item
              quantity:
                type: integer
                minimum: 1
                maximum: 100

    AccountPricing:
      type: object
      required: [per_user_rate, per_did_rate, per_voiceai_location_rate]
      properties:
        per_user_rate:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Agreed retail price per user seat, per month, in cents.
        per_did_rate:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Agreed retail price per phone number, per month, in cents.
        per_voiceai_location_rate:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Agreed retail price per VoiceAI-enabled location, per month, in cents.

    AccountPricingRequest:
      type: object
      required: [per_user_rate, per_did_rate, per_voiceai_location_rate]
      properties:
        per_user_rate:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Agreed retail price per user seat, per month, in cents
        per_did_rate:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Agreed retail price per phone number, per month, in cents
        per_voiceai_location_rate:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Agreed retail price per VoiceAI-enabled location, per month, in cents

    TosAcceptance:
      type: object
      description: |
        A recorded acceptance of the subscription agreement. Captures the
        evidence of acceptance (timestamp, IP address, user agent) together with
        a snapshot of the pricing agreed to, so consent is provable against the
        specific price shown even if pricing later changes.
      properties:
        accepted_at:
          type: string
          format: date-time
          description: When the agreement was accepted.
        ip:
          type: string
          description: |
            IP address of the client that recorded the acceptance — the account
            owner's browser for in-portal acceptance, or your integration's
            backend when your platform accepts on the owner's behalf via API.
        user_agent:
          type: string
          description: |
            User agent of the client that recorded the acceptance — the account
            owner's browser for in-portal acceptance, or your integration's
            client when your platform accepts on the owner's behalf via API.
        pricing:
          $ref: '#/components/schemas/AccountPricing'
      required:
        - accepted_at
        - ip
        - pricing

    Tos:
      type: object
      description: |
        The account's subscription-agreement resource — the current agreement
        document plus the account's acceptance state. The agreed pricing is
        included only when requested via `expand[]=pricing`.
      required: [version, url, content, body, acceptance]
      properties:
        version:
          type: string
          description: Version identifier of the agreement currently in effect (date-based, `YYYY-MM-DD`; it is also the effective date). Echo it back in the acceptance request so the server can reject acceptance of stale text.
        url:
          type: string
          description: Canonical URL of the full agreement.
        content:
          type: string
          description: Short affirmation the customer ticks to accept (the clickwrap checkbox label, including the 911/E911 acknowledgement).
        body:
          type: string
          description: Full agreement text (HTML) shown to the customer. Render this as the agreement body; `content` is the short affirmation that accompanies it.
        acceptance:
          anyOf:
            - $ref: '#/components/schemas/TosAcceptance'
            - type: 'null'
          description: The current acceptance, or null if the account has not accepted.
        pricing:
          anyOf:
            - $ref: '#/components/schemas/AccountPricing'
            - type: 'null'
          description: The account's agreed pricing. Included only when `expand[]=pricing` is requested; null when pricing has not been set.

    TosAcceptanceRequest:
      type: object
      required: [version]
      properties:
        version:
          type: string
          description: The agreement version being accepted; must match the current version. The evidence of acceptance (timestamp, IP, user agent) and its provenance are derived server-side from the request and authenticated actor, not supplied here.

    EntityId:
      type: string
      description: Unique identifier
      example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'

    EntityName:
      type: [string, 'null']
      minLength: 1
      maxLength: 255
      description: Display name
      example: 'Spineline'

    CreatedAt:
      type: string
      format: date-time
      description: Timestamp when it was created
      example: '2025-10-17T14:30:00Z'

    UpdatedAt:
      type: string
      format: date-time
      description: Timestamp when it was last updated
      example: '2025-10-17T14:30:00Z'

    ClientSecret:
      type: string
      description: JWT token for account-scoped authentication
      example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYWNjdF8wMWgyeGNlanF0ZjJuYnJleHgzdnFqaHA0MSIsImp0aSI6IjAxaDJ4Y2VqcXRmMm5icmV4eDN2cWpocDQ2IiwiZXhwIjoxNzI2NzU3MjAwfQ.1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2'

    SessionExpiresAt:
      type: string
      format: date-time
      description: When the session expires
      example: '2025-11-13T15:00:00Z'

    # List Response Base Properties

    ListObject:
      type: string
      enum: [list]
      description: String representing the object's type. Always "list" for list responses.

    ListUrl:
      type: string
      description: The URL for accessing this list.
      example: '/v1/accounts'

    NextPageUrl:
      type: [string, 'null']
      description: |
        URL to fetch the next page of results. If `null`, there are no more pages.
        The URL includes all necessary parameters including the pagination cursor.
      example: '/v1/accounts?page=eyJzdGFydGluZ19hZnRlciI6ImFjY3RfMDFoMnhjZWpxdGYybmJyZXh4M3ZxamhwNDEiLCJsaW1pdCI6MTB9'

    PreviousPageUrl:
      type: [string, 'null']
      description: |
        URL to fetch the previous page of results. If `null`, this is the first page.
        The URL includes all necessary parameters including the pagination cursor.
      example: '/v1/accounts?page=eyJlbmRpbmdfYmVmb3JlIjoiYWNjdF8wMWgyeGNlanF0ZjJuYnJleHgzdnFqaHA0MSIsImxpbWl0IjoxMH0'

    # Core Models

    Account:
      type: object
      description: A customer organization of a vSaaS platform (e.g., Jones Chiropractic using Spineline)
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        email:
          type: [string, 'null']
          format: email
          description: Email address for the account (optional)
          example: 'contact@example.com'
        name:
          type: [string, 'null']
          description: Company or organization name
          example: 'Acme Corp'
        phone:
          type: [string, 'null']
          description: Phone number in E.164 format
          example: '+12125550100'
        primary_contact_name:
          type: [string, 'null']
          description: Name of the primary contact
          example: 'Jane Doe'
        config:
          $ref: '#/components/schemas/AccountConfig'
        default_outbound_did:
          type: [string, 'null']
          description: ID of the phone number to use as the default outbound caller ID. When null, the first outbound-enabled number is used.
        hold_music_clip:
          type: [string, 'null']
          description: |
            ID of an audio clip on the account played in a loop to held callers
            (regular SIP hold, parking, queue wait). When null, callers hear the
            platform default hold music.
        main_location:
          type: [string, 'null']
          description: |
            ID of the account's main location — the fallback used for any user
            or phone number that doesn't have a specific location of its own.
            A user with no device (e.g., WebRTC-only) is considered roaming and
            resolves to this location; a phone number not tied to a specific
            location is similarly roaming. Used to determine tax
            jurisdiction and regulatory fees for roaming users and numbers.
            E911 dispatch uses per-device location, not this field. Set via
            POST /v1/accounts/{account_id}. Cannot be cleared once set — only
            swap to another active location is accepted. Null only during
            onboarding before the first location is created.
        default_button_template:
          type: [string, 'null']
          description: |
            ID of the account's default button template. Compatible deskphones
            and DECT handsets created after this is set inherit it at creation
            time; existing devices are not changed retroactively. Null when the
            account has no default.
        provisioning_base_url:
          type: [string, 'null']
          readOnly: true
          description: The full provisioning base URL for this account. Phones use this URL prefix for auto-provisioning configuration files.
          example: 'https://prov.netvoice.io/a/abc12345'
        billing_address:
          oneOf:
            - $ref: '#/components/schemas/Address'
            - type: 'null'
          description: The account's billing/business address, geocoded via address validation. Null if not set.
        onboarding_complete:
          type: boolean
          readOnly: true
          description: |
            True when every required onboarding step is satisfied by real
            account data: a business profile with a location, at least one team
            member, an active phone number with E911 provisioned, and a device
            assigned to a user.
        tos_status:
          type: string
          readOnly: true
          enum:
            - signed
            - unsigned
            - not_required
          description: |
            Subscription-agreement (SSA/TOS) coverage status. `signed` — a live
            account whose acceptance matches the current agreement version;
            `unsigned` — a live account with no acceptance, or one accepted
            against a superseded version (a version bump re-opens the gate);
            `not_required` — a non-live (sandbox/demo) account, which is never
            prompted. Filter the list with `?tos_status=`. The full agreement
            document and the evidence-bearing acceptance are on
            `GET /v1/accounts/{account_id}/tos`.
        tos_accepted_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          readOnly: true
          description: When the current agreement was accepted; null unless `tos_status` is `signed`.
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - config
        - created_at

    WebhookEventType:
      type: string
      description: |
        A webhook event type, or `*` to subscribe to every event. See the
        [Webhook Events](/guides/webhook-events#event-types) guide for the
        payload of each.
      enum:
        - '*'
        - call.initiated
        - call.incoming
        - call.ringing
        - call.mobile_push_wakeup
        - call.answered
        - call.end
        - call.transfer
        - call.emergency
        - fax.received
        - fax.delivered
        - fax.failed
        - voicemail.new
        - recording.available
        - recording.failed
        - recording.transcription.complete
        - recording.summary.complete
        - voicemail.transcription.complete
        - voicemail.summary.complete
        - queue.call.queued
        - queue.call.dispatched
        - queue.call.answered
        - queue.call.abandoned
        - queue.call.timed_out
        - queue.call.completed
        - queue.call.callback_requested
        - queue.call.callback_attempted
        - queue.call.callback_failed

    WebhookEndpoint:
      type: object
      description: |
        A URL that receives event notifications. Endpoints are mode-scoped: an
        endpoint only receives events from accounts whose mode matches its
        `livemode`. The mode is fixed at creation by the API key used and
        cannot be changed.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        url:
          type: string
          description: HTTPS URL that receives event POST requests.
          example: 'https://example.com/webhooks'
        livemode:
          type: boolean
          description: |
            Whether this endpoint receives live-mode events. `true` endpoints
            receive events from live accounts; `false` endpoints receive events
            from sandbox accounts. Set from the API key's mode at creation;
            immutable thereafter.
        enabled_events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          description: |
            Event types this endpoint is subscribed to, or `["*"]` for all
            events.
          example: ['*']
        status:
          type: string
          enum: [enabled, disabled]
          description: A disabled endpoint receives no events.
        description:
          type: [string, 'null']
          description: An optional description for the endpoint.
        secret:
          type: string
          description: |
            The signing secret used to verify webhook signatures. Returned
            **only** in the response that creates the endpoint; omitted on
            list, retrieve, and update. Store it securely.
          example: 'whsec_3f9a...'
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
      required:
        - id
        - url
        - livemode
        - enabled_events
        - status
        - description
        - created_at

    CreateWebhookEndpointRequest:
      type: object
      properties:
        url:
          type: string
          description: HTTPS URL that will receive event POST requests.
          example: 'https://example.com/webhooks'
        enabled_events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          description: |
            Event types to subscribe to, or `["*"]` for all events. Defaults to
            `["*"]` when omitted.
        description:
          type: string
          description: An optional description for the endpoint.
      required:
        - url

    UpdateWebhookEndpointRequest:
      type: object
      description: |
        All fields are optional. `livemode` is fixed at creation and cannot be
        updated.
      properties:
        url:
          type: string
          description: HTTPS URL that will receive event POST requests.
        enabled_events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          description: Event types to subscribe to, or `["*"]` for all events.
        status:
          type: string
          enum: [enabled, disabled]
        description:
          type: [string, 'null']
          description: Pass null to clear the description.

    AccountConfig:
      type: object
      description: Account-level configuration settings
      properties:
        region:
          type: string
          description: ISO 3166-1 alpha-2 country code for the account. Used for region-specific features. Defaults to "US".
          example: 'US'
        extension_length:
          type: integer
          minimum: 3
          maximum: 6
          description: Number of digits for extension numbers (3-6). Defaults to 4.
          example: 4
        transcription_enabled:
          type: boolean
          description: Whether calls are transcribed. Defaults to true.
          example: true
        recording_enabled:
          type: boolean
          description: Whether call audio recordings are retained. Defaults to true.
          example: true
        redaction_enabled:
          type: boolean
          description: Whether sensitive information (PII) is redacted from transcripts and audio recordings. When enabled, redacted words appear as `[PII]` in transcripts, the matching audio is silenced in the recording, calls are transcribed in English only, and a call's recording is available for download only after redaction completes. Defaults to false.
          example: false
        timezone:
          type: string
          description: IANA timezone for the account (e.g., "America/New_York"). Used as the default timezone for schedules when not explicitly set. Defaults to "UTC".
          example: 'America/New_York'
        max_phone_numbers:
          type: integer
          minimum: 1
          description: Maximum number of phone numbers the account can hold. Defaults to 25.
          example: 25
        parking_timeout_seconds:
          type: integer
          minimum: 1
          description: Duration in seconds before a parked call rings back the parker. Defaults to 300 (5 minutes).
          example: 300
        fax_inbound_per_hour:
          type: integer
          minimum: 1
          maximum: 1000
          description: Maximum inbound faxes accepted per rolling hour across the account. Over-limit faxes are rejected. Omit to use the default (25).
          example: 25
        fax_outbound_per_hour:
          type: integer
          minimum: 1
          maximum: 1000
          description: Maximum outbound faxes submitted per rolling hour across the account. Omit to use the default (25).
          example: 25
        default_agent_visible:
          type: [boolean, 'null']
          description: >-
            Account-level override for whether the managed AI agent is offered
            when creating a voice app. Omit (null) to inherit the platform
            default, `true` to always show it, `false` to hide it. When hidden,
            creating a managed AI agent is rejected.
          example: false
        e911_notification:
          $ref: '#/components/schemas/E911NotificationConfig'

    E911NotificationConfig:
      type: object
      description: |
        Email recipients alerted when an emergency number (911 or a regional
        equivalent) is dialed.

        On an account, recipients are alerted for every call on the account.
        On a location, recipients are alerted only for calls placed from that
        location, in addition to the account-level recipients. The two lists
        are unioned at delivery time.

        The `call.emergency` webhook event is delivered to the account's
        platform webhook URL regardless of this setting.
      properties:
        emails:
          type: array
          description: Email recipients for emergency call alerts.
          items:
            type: string
            format: email
          example: ['security@example.com', 'frontdesk@example.com']

    LocationConfig:
      type: object
      description: Per-location configuration overrides
      properties:
        e911_notification:
          $ref: '#/components/schemas/E911NotificationConfig'

    User:
      type: object
      description: A person with phone service (may have multiple devices/endpoints)
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        name:
          allOf:
            - $ref: '#/components/schemas/EntityName'
        email:
          type: [string, 'null']
          format: email
          description: Email address (optional)
          example: 'alice@spineline.dev'
        outbound_caller_id_did:
          type: [string, 'null']
          description: ID of the phone number to use as this user's outbound caller ID, overriding the account default. When null, the account default is used.
        voicemail_pin:
          type: [string, 'null']
          description: Numeric PIN for remote voicemail access (4-10 digits). When null, no PIN is set.
          example: '1234'
        config:
          $ref: '#/components/schemas/UserConfig'
        do_not_disturb:
          type: boolean
          description: |
            When `true`, the calling path declines calls to this user (server-side do-not-disturb). Independent of the user's live availability, which is reported separately on the presence resource.
          example: false
        extensions:
          description: |
            List of extensions assigned to this user. Only included when `expand[]=extensions` is requested.
          type: object
          properties:
            object:
              type: string
              enum: [list]
            data:
              type: array
              items:
                $ref: '#/components/schemas/Extension'
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - do_not_disturb

    UserConfig:
      type: object
      description: User-level configuration settings
      properties:
        voicemail_notifications:
          $ref: '#/components/schemas/VoicemailNotificationConfig'
        find_me_follow_me:
          $ref: '#/components/schemas/FMFM'
        mobile_push_wakeup:
          type: boolean
          description: |
            Set to `true` when your application can deliver push notifications to wake this user's mobile app.
            When enabled and the user has no active web/mobile calling session, an incoming call is held for a
            short wake-up window instead of skipping the app immediately — giving your `call.mobile_push_wakeup` webhook →
            push notification → app wake-up → connect flow time to pick up the still-ringing call.
            When disabled (default), routing moves on without waiting.
          default: false
          example: true

    FMFM:
      type: object
      description: Find Me / Follow Me call routing configuration. Steps are tried sequentially; within each step, all targets ring simultaneously.
      required:
        - steps
        - fallback
      properties:
        steps:
          type: array
          items:
            $ref: '#/components/schemas/FMFMStep'
          minItems: 1
          maxItems: 5
        fallback:
          type: string
          enum: [voicemail, hangup, target]
          description: Action when all steps are exhausted without an answer. When `target`, routes to the resource specified by `fallback_target`.
        fallback_target:
          type: string
          description: ID of the routing target for fallback. Required when fallback is `target`. Accepts user, ring group, voice app, dial plan, or shared voicemail IDs.

    FMFMStep:
      type: object
      description: A single ring step. All targets ring simultaneously with a shared timeout.
      required:
        - targets
        - timeout
      properties:
        targets:
          type: array
          items:
            $ref: '#/components/schemas/FMFMTarget'
          minItems: 1
          maxItems: 10
        timeout:
          type: integer
          minimum: 1
          maximum: 120
          description: How long to ring all targets in this step (seconds).

    FMFMTarget:
      type: object
      description: A target to ring within a step.
      required:
        - type
      properties:
        type:
          type: string
          enum: [external, user]
          description: |
            Target type:
            - `external` — dial a PSTN phone number.
            - `user` — ring a user's SIP endpoints (can be the current user or another user).
        number:
          type: string
          description: Phone number in E.164 format. Required when type is `external`.
          example: '+15551234567'
        id:
          type: string
          description: ID of the user to ring. Required when type is `user`.

    VoicemailNotificationConfig:
      type: object
      description: Voicemail email notification preferences
      properties:
        enabled:
          type: boolean
          description: Whether voicemail email notifications are enabled (user voicemails only)
          default: false
        attach_audio:
          type: boolean
          description: Attach the voicemail audio file (MP3) to the email. Forced true when delete_after_email is true.
          default: true
        include_summary:
          type: boolean
          description: Include the AI-generated summary in the email. When true, email is sent after transcription completes.
          default: true
        include_transcript:
          type: boolean
          description: Include the full transcript in the email. When true, email is sent after transcription completes.
          default: true
        delete_after_email:
          type: boolean
          description: Automatically delete the voicemail after the notification email is sent. Implies attach_audio.
          default: false

    Voicemail:
      type: object
      description: |
        A voicemail message with audio recording. Each voicemail has exactly
        one owner — either a user or a shared voicemail box.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        owner:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: ID of the owner — either the user who received the voicemail or the shared voicemail box it was left in.
        from_number:
          type: string
          maxLength: 20
          description: Caller's phone number
          example: '+14155551234'
        from_name:
          type: [string, 'null']
          maxLength: 255
          description: Caller's name (if available from caller ID)
          example: 'John Smith'
        duration_seconds:
          type: integer
          minimum: 0
          description: Length of the voicemail recording in seconds
          example: 42
        format:
          type: string
          maxLength: 10
          description: Audio format
          example: 'mp3'
        audio_url:
          type: string
          format: uri
          description: URL for downloading the audio recording
          example: 'https://voicemails.dialstack.ai/550e8400-e29b-41d4-a716-446655440000/20251018-143000_John_Smith.mp3'
        is_read:
          type: boolean
          description: Whether the voicemail has been listened to
          example: false
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        read_at:
          type: [string, 'null']
          format: date-time
          description: When the voicemail was marked as read
          example: '2025-10-18T15:00:00Z'
        summary:
          type: [string, 'null']
          description: AI-generated summary of the voicemail content. Null if transcription not yet processed.
          example: 'John Smith called to reschedule his Thursday appointment to Friday.'
        sentiment:
          oneOf:
            - $ref: '#/components/schemas/Sentiment'
            - type: 'null'
          description: |
            AI-derived sentiment of the voicemail transcript. Single-channel
            audio, so no `local`/`remote` breakdown. Null when not analyzed.
        call:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - $ref: '#/components/schemas/CallLog'
            - type: 'null'
          description: |
            The call that produced this voicemail. By default this is the
            call's id; when `expand[]=call` is requested it is the full call
            object (identical to `GET /v1/calls/{call_id}`). Null for
            voicemails with no associated call.
      required:
        - id
        - owner
        - from_number
        - duration_seconds
        - format
        - audio_url
        - is_read
        - created_at
        - call

    VoicemailGreetingResponse:
      type: object
      description: |
        A custom voicemail greeting that replaces the system-default prompts
        for a user mailbox or a shared voicemail box. Each (owner, type) pair
        has at most one greeting — re-uploading overwrites.
      properties:
        owner:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: ID of the owner — either the user whose mailbox the greeting belongs to, or the shared voicemail box it belongs to.
        greeting_type:
          type: string
          description: |
            The greeting variant. Today only `unavailable` is supported —
            the full custom greeting that replaces the system prompts
            (caller hears it when the owner does not answer).
          enum: [unavailable]
        format:
          type: string
          description: Audio format of the stored greeting.
          enum: [wav]
          example: 'wav'
        duration_seconds:
          type: number
          format: float
          minimum: 0
          description: Length of the greeting audio in seconds.
          example: 12.4
        size_bytes:
          type: integer
          format: int64
          minimum: 0
          description: Size of the stored greeting audio in bytes (post-transcode).
          example: 98560
        url:
          type: string
          format: uri
          description: Short-lived signed URL (5 minutes) for downloading the greeting audio.
        updated_at:
          type: string
          format: date-time
          description: When the greeting was last uploaded or replaced.
      required:
        - owner
        - greeting_type
        - format
        - duration_seconds
        - size_bytes
        - url
        - updated_at

    AuditLogEntry:
      type: object
      description: A single audit log entry recording an API operation
      properties:
        id:
          type: string
          description: Unique identifier for the audit log entry
        actor:
          type: string
          description: Identifier of the actor that performed the operation
        event:
          type: string
          description: Type of event (e.g., `user.create`, `auth.denied`)
        resource:
          type: string
          description: Identifier of the affected resource
        outcome:
          type: string
          description: Result of the operation
          enum: [success, failure, denied]
        ip_address:
          type: string
          description: Client IP address
        timestamp:
          type: string
          format: date-time
          description: When the event occurred
      required:
        - id
        - event
        - outcome
        - ip_address
        - timestamp

    DIDSummary:
      type: object
      description: |
        Compact phone number (DID) reference embedded in a resource when
        `expand[]=did` is requested (call logs, faxes). Intentionally limited to
        identity and the dialable number — the full phone number resource
        (routing, caller ID, directory listing, configuration) is available via
        `/v1/phone-numbers`.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        phone_number:
          type: string
          description: The DID in E.164 format.
          example: '+14155559876'
      required:
        - id
        - phone_number

    CallLog:
      type: object
      description: |
        Record of a completed or attempted call. The `id` is an opaque call
        log identifier — a single call log may span multiple underlying call
        legs, so it does not correspond to any single leg.

        The record is lifecycle-spanning: it can be retrieved while the call is
        still live. While a call is in progress it is a sparse projection —
        `status`, `to_label`, `ended_at`, `duration_seconds`, `answered_at`,
        `connected_at`, `hangup_cause`, `summary`, and `recording_url` are null
        until the call completes, and `quality_metrics` is an empty array.
        `to_number` is present live for outbound and internal calls (whose
        destination is known at call start) but null for inbound calls (the
        routed destination is resolved later), and `from_label` carries the
        caller's display name when one is available; see each field for its live
        behavior.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        user:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - type: 'null'
          description: The user associated with this call
        endpoint:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - type: 'null'
          description: The endpoint (device) that handled this call
        did:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - $ref: '#/components/schemas/DIDSummary'
            - type: 'null'
          description: |
            The phone number (DID) associated with this call. By default this
            is the DID's id; when `expand[]=did` is requested it is a compact
            DID object (id and phone number only). Null for calls without a DID.
        direction:
          type: string
          enum: [inbound, outbound, internal]
          description: Call direction (inbound from PSTN, outbound to PSTN, or internal extension-to-extension)
          example: 'inbound'
        from_number:
          type: string
          minLength: 0
          maxLength: 20
          description: Caller's phone number or originating extension. May be empty when the originating identity could not be determined.
          example: '+14155551234'
        from_label:
          type: [string, 'null']
          maxLength: 80
          description: The caller's display name. For internal callers this is the calling user's display name (the caller is always a user's device); for external callers it is the caller ID name (CNAM). While a call is live it is the name captured at call start. Null when no name is available. For external callers the CNAM is not interpreted, only trimmed and limited to 80 characters, so it may be a locality rather than a person ("LA MESA CA"), a placeholder ("WIRELESS CALLER"), or a restatement of `from_number` — check for the latter before rendering the name beside the number.
          example: 'Front Desk'
        to_number:
          type: [string, 'null']
          minLength: 0
          maxLength: 20
          description: The call's destination — an E.164 phone number for PSTN destinations, or the internal extension number the call was routed to for calls that terminate inside the account (including inbound calls to one of the account's phone numbers). May be empty when the destination could not be determined. While a call is live it is present when the destination is known at call start — outbound calls and internal extension-to-extension calls; for an inbound call the routed destination is resolved later, so `to_number` is null for the entire time the call is in flight and is populated once the call completes. Note that webhook events report the number as dialed instead, so an inbound call's webhook `to_number` (the E.164 phone number) can differ from this field (the routed extension).
          example: '+14155559876'
        to_label:
          type: [string, 'null']
          description: Display label for the recipient, resolved from the extension's target name (user, dial plan, voice app, or ring group). Null if the number does not match an extension.
          example: 'Sales Queue'
        started_at:
          type: string
          format: date-time
          description: When the call was initiated
          example: '2025-10-18T14:30:00Z'
        answered_at:
          type: [string, 'null']
          format: date-time
          description: >-
            When the call was answered at the signalling level. A greeting,
            menu, or voice app answering the media path counts, so for inbound
            calls that play one this is the platform answer (≈ started_at), not
            when a person picked up. Null for unanswered calls. Use connected_at
            for when a person or external number actually took the call.
          example: '2025-10-18T14:30:05Z'
        connected_at:
          type: [string, 'null']
          format: date-time
          description: >-
            When the winning leg — a user's device or an external-number
            fallback — answered, i.e. when live conversation began. Null when
            the call never connected to anyone: the caller hung up during the
            greeting or while ringing, or the call was answered by voicemail.
            For a direct call with no greeting this equals answered_at.
          example: '2025-10-18T14:31:02Z'
        ended_at:
          type: [string, 'null']
          format: date-time
          description: When the call ended. Null if the call is still in flight.
          example: '2025-10-18T14:35:30Z'
        duration_seconds:
          type: [integer, 'null']
          minimum: 0
          description: Call duration in seconds (from answer to hangup). Null for unanswered calls.
          example: 325
        status:
          type: [string, 'null']
          enum: [completed, no-answer, busy, failed, voicemail, null]
          description: Final call status, derived once the call ends. Null while the call is still live (in progress) — the status values are terminal only.
          example: 'completed'
        hangup_cause:
          type: [integer, 'null']
          description: >-
            The Q.850 hangup cause code of the destination leg when the call
            could not be connected — the standard telephony cause number (for
            example, 17 user busy, 21 call rejected, 38 network out of order).
            Set even if the call was answered on the originating side (for
            example, a click-to-call where your own phone picks up before the
            destination is dialed). Null when the call connected normally or no
            cause was recorded.
          example: 21
        summary:
          type: [string, 'null']
          description: AI-generated summary of the call. Null if call wasn't recorded or transcript not yet processed.
          example: 'Customer called to reschedule appointment from Thursday to Friday due to work conflict.'
        sentiment:
          oneOf:
            - $ref: '#/components/schemas/Sentiment'
            - type: 'null'
          description: |
            AI-derived sentiment of the call transcript. Null if the call was
            not recorded, the transcript is not yet processed, or the analysis
            did not produce a usable result.
        recording_url:
          type: [string, 'null']
          format: uri
          description: Signed URL to download the call recording audio. Null if the call was not recorded. The URL expires after 10 minutes.
          example: 'https://cdn.example.com/recordings/2026-03-05/call.wav?Expires=1709654400&Signature=abc123'
        quality_metrics:
          type: array
          description: RTP quality metrics per call leg. Each call may have multiple legs (e.g., PSTN + endpoint for inbound calls). Empty array for unanswered calls.
          items:
            $ref: '#/components/schemas/QualityMetricLeg'
      required:
        - id
        - user
        - endpoint
        - did
        - did
        - direction
        - from_number
        - from_label
        - to_number
        - to_label
        - started_at
        - answered_at
        - connected_at
        - ended_at
        - duration_seconds
        - status
        - hangup_cause
        - summary
        - recording_url
        - quality_metrics

    QualityMetricLeg:
      type: object
      description: RTP quality metrics for a single call leg (PSTN or endpoint)
      properties:
        leg:
          type: string
          enum: [pstn, endpoint]
          description: Type of call leg
          example: 'endpoint'
        endpoint:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - type: 'null'
          description: The endpoint ID for endpoint legs. Null for PSTN legs.
        jitter_ms:
          type: [number, 'null']
          format: float
          minimum: 0
          description: Average jitter in milliseconds during the call leg
          example: 5.2
        jitter_min_ms:
          type: [number, 'null']
          format: float
          minimum: 0
          description: Minimum jitter observed during the call leg in milliseconds
          example: 1.0
        jitter_max_ms:
          type: [number, 'null']
          format: float
          minimum: 0
          description: Maximum jitter observed during the call leg in milliseconds
          example: 15.5
        jitter_stddev_ms:
          type: [number, 'null']
          format: float
          minimum: 0
          description: Standard deviation of jitter during the call leg in milliseconds
          example: 2.3
        packet_loss_pct:
          type: [number, 'null']
          format: float
          minimum: 0
          maximum: 100
          description: Packet loss percentage during the call leg
          example: 0.5
        rtt_ms:
          type: [number, 'null']
          format: float
          minimum: 0
          description: Average round-trip time (RTT) in milliseconds during the call leg
          example: 45.0
        rtt_min_ms:
          type: [number, 'null']
          format: float
          minimum: 0
          description: Minimum round-trip time observed during the call leg in milliseconds
          example: 30.0
        rtt_max_ms:
          type: [number, 'null']
          format: float
          minimum: 0
          description: Maximum round-trip time observed during the call leg in milliseconds
          example: 80.0
        rtt_stddev_ms:
          type: [number, 'null']
          format: float
          minimum: 0
          description: Standard deviation of round-trip time during the call leg in milliseconds
          example: 12.5
        rx_count:
          type: [integer, 'null']
          format: int32
          minimum: 0
          description: Total number of RTP packets received during the call leg
          example: 10000
        tx_count:
          type: [integer, 'null']
          format: int32
          minimum: 0
          description: Total number of RTP packets transmitted during the call leg
          example: 10050
        mos:
          type: [number, 'null']
          format: float
          minimum: 1.0
          maximum: 4.5
          description: Mean Opinion Score (MOS) computed from RTP quality metrics using ITU-T G.107 E-model. Ranges from 1.0 (bad) to 4.5 (excellent).
          example: 4.2
      required:
        - leg

    Sentiment:
      type: object
      description: |
        AI-derived sentiment of a call or voicemail transcript, generated
        automatically alongside the summary once transcription completes.

        Two independent axes:

        - `score` is the emotional **leaning**, from -1.0 (strongly negative)
          through 0 (neutral) to +1.0 (strongly positive). `overall` is derived
          from it, so the two never disagree.
        - `magnitude` is how strongly feeling was expressed **at all**, from 0.0
          to 1.0. It is unsigned.

        Reading them together matters: a call where one side was angry and the
        other delighted scores near 0 with a **high** magnitude, while a routine
        call scores near 0 with a **low** one. Both are `neutral` by label.

        Most business calls are neutral with a low magnitude — routine,
        businesslike exchanges score near 0 even when both parties are perfectly
        polite — so anything else is meaningful rather than routine.

        Sentiment reflects how the participants feel about the interaction, not
        the subject matter: a calm conversation about an unpleasant topic is
        neutral, while a caller who is frustrated or distressed is negative even
        if the exchange stays cordial.
      properties:
        overall:
          type: string
          enum: [positive, neutral, negative]
          description: |
            Overall sentiment, derived from `score`. Check `magnitude` before
            reading `neutral` as "nobody felt anything".
          example: 'negative'
        score:
          type: number
          format: float
          minimum: -1
          maximum: 1
          description: Emotional leaning, from -1.0 (strongly negative) to +1.0 (strongly positive).
          example: -0.7
        magnitude:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: |
            Unsigned strength of expressed feeling, from 0.0 (flat and
            businesslike) to 1.0 (intense).
          example: 0.9
        local:
          allOf:
            - $ref: '#/components/schemas/ChannelSentiment'
            - description: |
                Sentiment of your user's side of the conversation. Absent for
                voicemail, whose audio is single-channel.
        remote:
          allOf:
            - $ref: '#/components/schemas/ChannelSentiment'
            - description: |
                Sentiment of the other party's side of the conversation. Absent
                for voicemail, whose audio is single-channel.
      required:
        - overall
        - score
        - magnitude

    ChannelSentiment:
      type: object
      description: Sentiment for one speaker on a two-party call.
      properties:
        sentiment:
          type: string
          enum: [positive, neutral, negative]
          description: This speaker's sentiment, derived from their `score`.
          example: 'negative'
        score:
          type: number
          format: float
          minimum: -1
          maximum: 1
          description: This speaker's emotional leaning, on the same -1.0 to +1.0 scale.
          example: -0.85
        magnitude:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: How strongly this speaker expressed feeling, 0.0 to 1.0.
          example: 0.95
      required:
        - sentiment
        - score
        - magnitude

    Transcript:
      type: object
      description: Call transcript containing transcription status and text
      properties:
        call_id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: The call this transcript belongs to
        status:
          type: string
          enum: [pending, processing, completed, failed]
          description: |
            Current status of the transcription:
            - `pending`: Recording uploaded, transcription not started
            - `processing`: Transcription in progress
            - `completed`: Transcription finished successfully
            - `failed`: Transcription failed
          example: 'completed'
        text:
          type: [string, 'null']
          description: Full text transcript. Null if transcription is not completed.
          example: 'Hello, this is Dr. Smith calling to confirm your appointment...'
        sentiment:
          oneOf:
            - $ref: '#/components/schemas/Sentiment'
            - type: 'null'
          description: |
            Sentiment derived from the transcript. Null while transcription is
            incomplete, or when the analysis did not produce a usable result.
      required:
        - call_id
        - status

    Recording:
      type: object
      description: Call recording metadata with a signed download URL
      properties:
        call_id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: The call this recording belongs to
        duration_seconds:
          type: [integer, 'null']
          minimum: 0
          description: Duration of the recording in seconds. Null if not available.
          example: 185
        file_size_bytes:
          type: [integer, 'null']
          minimum: 0
          description: Size of the recording file in bytes. Null if not available.
          example: 2960000
        download_url:
          type: string
          format: uri
          description: Signed URL to download the recording audio file. Expires after 10 minutes.
          example: 'https://cdn.example.com/recordings/2026-03-05/call.wav?Expires=1709654400&Signature=abc123'
        expires_at:
          type: string
          format: date-time
          description: When the download URL expires
          example: '2026-03-05T15:40:00Z'
      required:
        - call_id
        - download_url
        - expires_at

    VoicemailTranscript:
      type: object
      description: Voicemail transcript containing transcription status and text
      properties:
        voicemail:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: The voicemail this transcript belongs to
        status:
          type: string
          enum: [pending, processing, completed, failed]
          description: |
            Current status of the transcription:
            - `pending`: Voicemail uploaded, transcription not started
            - `processing`: Transcription in progress
            - `completed`: Transcription finished successfully
            - `failed`: Transcription failed
          example: 'completed'
        text:
          type: [string, 'null']
          description: Full text transcript. Null if transcription is not completed.
          example: 'Hi, this is John Smith calling about my appointment tomorrow...'
        sentiment:
          oneOf:
            - $ref: '#/components/schemas/Sentiment'
            - type: 'null'
          description: |
            Sentiment derived from the transcript. Voicemail audio is
            single-channel, so this never carries the `local`/`remote` breakdown a
            call transcript does. Null when not analyzed.
      required:
        - voicemail
        - status

    PhoneNumber:
      type: object
      description: A phone number (DID) assigned to an account for inbound/outbound calling
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        phone_number:
          type: string
          maxLength: 20
          description: Phone number in E.164 format
          example: '+14155551234'
        status:
          type: string
          enum: [active, inactive, released]
          description: Current status of the phone number
          example: 'active'
        outbound_enabled:
          type: boolean
          description: Whether this phone number can be used for outbound calling
          example: true
        fax_enabled:
          type: boolean
          description: |
            Whether this number is set up for faxing. When true, inbound calls
            are received as faxes (its inbound routing target is bypassed but
            preserved, and resumes if fax is disabled). The number can still be
            used as an outbound caller ID.
          example: false
        config:
          $ref: '#/components/schemas/DIDConfig'
        expires_at:
          type: ['string', 'null']
          format: date-time
          description: When the phone number is scheduled to be released back to the carrier. Set on temporary numbers issued during signup; null on permanent numbers.
          example: null
        disconnected_at:
          type: ['string', 'null']
          format: date-time
          description: When the phone number was released back to the carrier. Null while the number is in service.
          example: null
        caller_id_name:
          type: ['string', 'null']
          maxLength: 15
          description: The caller ID name (CNAM) configured for this phone number, or null if not set
          example: 'ACME Corp'
        caller_id_prefix:
          type: ['string', 'null']
          maxLength: 32
          description: >-
            A prefix added to the inbound caller's display name shown on the
            answering device (e.g. `[Acme] John Smith`). Useful when calls to
            this number are answered on behalf of a specific brand or business.
            Unlike `caller_id_name` (the outbound caller ID name), this affects
            only what the answering party sees on inbound calls; it does not
            change call records. Null when not set.
          example: '[Acme]'
        directory_listing_name:
          type: ['string', 'null']
          description: The business name used for directory listing (411/white pages), or null if not set
          example: 'Acme Corporation'
        directory_listing_type:
          type: string
          enum: [listed, non_listed, non_published, non_registered]
          description: How the number appears in directory services
          example: 'listed'
        directory_listing_location:
          type: ['string', 'null']
          description: ID of the location whose address is published in the directory listing, or null
          example: null
        routing_target:
          type: [string, 'null']
          description: ID of the target that inbound calls to this number are routed to, or null when no target is set. Always null when `inbound_routing` is `drop`.
          example: null
        inbound_routing:
          type: string
          enum: [default, drop]
          description: >-
            How inbound calls to this number are handled. `default` follows
            `routing_target` — routing to the target when one is set, otherwise
            ringing all users on the account. `drop` deliberately drops inbound
            calls (no ring, no message); `routing_target` is null in that case.
          example: 'default'
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - phone_number
        - status
        - outbound_enabled
        - fax_enabled
        - expires_at
        - disconnected_at
        - routing_target
        - inbound_routing
        - created_at
        - updated_at

    SmsPortOutWindow:
      type: object
      description: |
        The SMS port-out window for a number. `expires_at` is the instant until
        which an inbound SMS port-out (messaging transfer) request is authorized;
        `null` means the window is locked and requests are denied.
      properties:
        expires_at:
          type: [string, 'null']
          format: date-time
          description: RFC 3339 timestamp the window stays open until, or null when locked.
      required:
        - expires_at

    DIDConfig:
      type: object
      description: Per-number configuration blob.
      properties:
        fax_notifications:
          $ref: '#/components/schemas/FaxNotificationConfig'

    FaxNotificationConfig:
      type: object
      description: Email notifications sent when a fax is received on this number.
      properties:
        recipients:
          type: array
          maxItems: 10
          items:
            type: string
            format: email
          description: Up to 10 email addresses notified when a fax is received on this number. An empty list disables notifications.
          example: ['fax@example.com']
        delete_documents:
          type: boolean
          description: >-
            When true, fax documents are not retained on the server, in both
            directions (defaults to false). Inbound: the received document is
            attached to the notification email as a PDF and the stored copy is
            removed after the email is sent. Outbound: the uploaded source
            document is deleted once the fax reaches a terminal status
            (`delivered` or `failed`). Either way the document is no longer
            retrievable in-app. When false, the notification email instead
            includes a time-bounded link to view the received document and
            documents are retained. Requires at least one entry in `recipients`:
            enabling this with an empty recipient list is rejected, since a
            received fax would be neither stored nor delivered.
          example: false
        attach_pdf:
          type: boolean
          deprecated: true
          description: >-
            Deprecated and retained only for backwards compatibility. It is
            accepted on input but ignored (not stored, no effect) and always
            returned as `true` (its historical default). Use `delete_documents`
            to control document handling. This field will be removed in a future
            release.

    AvailablePhoneNumber:
      type: object
      description: A phone number available for purchase
      properties:
        phone_number:
          type: string
          maxLength: 20
          description: Phone number in E.164 format
          example: '+19195551234'
        city:
          type: string
          description: City where the number is registered
          example: 'RALEIGH'
        state:
          type: string
          description: Two-letter state abbreviation
          example: 'NC'
        rate_center:
          type: string
          description: Rate center for the number
          example: 'RALEIGH'
        lata:
          type: string
          description: Local access and transport area
          example: '422'
      required:
        - phone_number

    NumberOrder:
      type: object
      description: |
        Tracks the status of a phone number purchase or disconnect request.
        Purchase orders acquire new numbers — number porting is not supported.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        order_type:
          type: string
          enum: [purchase, disconnect]
          description: |
            `purchase` — acquire new phone numbers.
            `disconnect` — release existing phone numbers back to the provider.
          example: 'purchase'
        status:
          type: string
          enum: [pending, complete, partial, failed]
          description: |
            `pending` — order submitted, awaiting provider confirmation.
            `complete` — all requested numbers were successfully provisioned (or released).
            `partial` — some numbers succeeded, others failed.
            `failed` — the entire order was rejected.
          example: 'pending'
        phone_numbers:
          type: array
          items:
            type: string
          description: Phone numbers included in the order (E.164 format)
          example: ['+19195551234', '+19195555678']
        completed_numbers:
          type: array
          items:
            type: string
          description: Phone numbers that completed successfully
          example: ['+19195551234']
        failed_numbers:
          type: array
          items:
            type: string
          description: Phone numbers that failed
          example: []
        error_message:
          type: [string, 'null']
          description: Error details if the order failed
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - order_type
        - status
        - phone_numbers
        - completed_numbers
        - failed_numbers
        - created_at
        - updated_at

    VoiceApp:
      type: object
      description: |
        A voice app handles calls via HTTP webhook notification and optional WebSocket
        audio streaming. When a call is routed to a voice app, DialStack sends an HTTP
        POST notification to the configured URL. The platform can then use the Update
        Call API to control the call (e.g., attach a WebSocket for bidirectional audio
        streaming, or transfer to another extension).

        Voice apps also receive notifications from Voice App (Notify) dial plan nodes,
        enabling real-time call monitoring via the Listeners API without affecting call routing.

        ## Webhook Events

        Voice apps receive two types of webhooks, distinguished by the `event` field:

        - `call.received` — The call has been routed to this voice app for handling.
          Your server takes control of the call via the Update Call API.
        - `call.notify` — A call is passing through a Voice App (Notify) node in a dial plan.
          The call continues routing normally. Use the Listeners API to stream audio.

        ## Webhook Notification

        When a call arrives, DialStack sends a POST request to the voice app's URL:

        ```
        POST https://your-app.example.com/calls
        Content-Type: application/json
        X-DialStack-Signature: t=1697634600,v1=5257a869...

        {
          "event": "call.received",
          "call_id": "call_...",
          "account_id": "acct_...",
          "voice_app_id": "va_...",
          "from_number": "+14155551234",
          "from_name": "John Smith",
          "to_number": "+14155559876"
        }
        ```

        **Signature Verification:**
        The `X-DialStack-Signature` header contains a timestamp and HMAC-SHA256 signature:
        - `t` = Unix timestamp (seconds) when signature was generated
        - `v1` = HMAC-SHA256 signature

        To verify: compute `HMAC-SHA256("${t}.${request_body}", voice_app_secret)` and compare.
        Reject timestamps older than 5 minutes to prevent replay attacks.

        **Response:** Return `200 OK` to acknowledge. Response body is ignored.

        **Delivery:** Failed webhooks are retried with exponential backoff (3 attempts, 5-second timeout).

        See [Voice Apps Guide](/guides/voice-apps) for complete examples.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          example: 'va_01h2xcejqtf2nbrexx3vqjhp49'
        name:
          allOf:
            - $ref: '#/components/schemas/EntityName'
          example: 'AI Receptionist'
        url:
          type: string
          format: uri
          description: HTTPS webhook URL for call control notifications
          example: 'https://ai.platform.example.com/calls'
        status:
          type: string
          enum: [active, inactive]
          description: Current status of the voice app
          example: 'active'
        secret:
          type: string
          description: Webhook signing secret for this voice app. Used to verify webhook signatures.
          example: 'whsec_abc123def456...'
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
        extensions:
          description: |
            List of extensions assigned to this voice app. Only included when `expand[]=extensions` is requested.
          type: object
          properties:
            object:
              type: string
              enum: [list]
            data:
              type: array
              items:
                $ref: '#/components/schemas/Extension'
      required:
        - id
        - name
        - url
        - status
        - secret
        - created_at

    TimeRange:
      type: object
      description: A time range within a specific day of the week
      properties:
        day:
          type: integer
          minimum: 0
          maximum: 6
          description: Day of the week (0=Sunday, 1=Monday, ..., 6=Saturday)
          example: 1
        start:
          type: string
          format: time-local
          description: Start time in HH:MM format (24-hour)
          example: '09:00'
        end:
          type: string
          format: time-local
          description: End time in HH:MM format (24-hour)
          example: '17:00'
      required:
        - day
        - start
        - end

    DateRange:
      type: object
      description: A date range for holidays (full days only)
      properties:
        start:
          type: string
          format: date
          description: Start date (YYYY-MM-DD)
          example: '2025-12-25'
        end:
          type: string
          format: date
          description: End date (YYYY-MM-DD), inclusive
          example: '2025-12-25'
      required:
        - start
        - end

    ScheduleHold:
      type: object
      description: |
        Temporary hold override for a schedule. When set, the hold value overrides
        the normal schedule calculation until the specified time.
      properties:
        value:
          type: boolean
          description: Override value (true=force open, false=force closed)
          example: false
        until:
          type: string
          format: date-time
          description: When the hold expires, as an absolute RFC3339 instant.
          example: '2025-12-20T22:00:00Z'
      required:
        - value
        - until

    Schedule:
      type: object
      description: |
        A business hours schedule for call routing. Schedules define when the business
        is open based on weekly time ranges and holidays. A temporary hold can override
        the schedule calculation.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          example: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the schedule
          example: 'Business Hours'
        timezone:
          type: [string, 'null']
          description: IANA timezone for interpreting times (e.g., "America/New_York"). If null, uses account default.
          example: 'America/New_York'
        ranges:
          type: array
          description: Weekly time ranges when the schedule is considered "open"
          items:
            $ref: '#/components/schemas/TimeRange'
          example:
            - day: 1
              start: '09:00'
              end: '17:00'
            - day: 2
              start: '09:00'
              end: '17:00'
        holidays:
          type: array
          description: Date ranges when the schedule is closed regardless of weekly ranges
          items:
            $ref: '#/components/schemas/DateRange'
          example:
            - start: '2025-12-25'
              end: '2025-12-25'
        hold:
          oneOf:
            - $ref: '#/components/schemas/ScheduleHold'
            - type: 'null'
          description: Temporary override for the schedule
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - name
        - ranges
        - created_at

    # Request Bodies

    AccountRequest:
      type: object
      properties:
        email:
          type: string
          format: email
          description: >-
            Email address for the account owner. Required when creating an
            account — it seeds the account owner identity.
          example: 'contact@example.com'
        name:
          type: string
          description: Company or organization name
          example: 'Acme Corp'
        phone:
          type: string
          description: Phone number in E.164 format
          example: '+12125550100'
        primary_contact_name:
          type: string
          description: >-
            Name of the primary contact (the account owner). Required when
            creating an account.
          example: 'Jane Doe'
        config:
          $ref: '#/components/schemas/AccountConfig'

    CreateAccountRequest:
      allOf:
        - $ref: '#/components/schemas/AccountRequest'
        - type: object
          required: [email, primary_contact_name, billing_address, pricing]
          properties:
            billing_address:
              $ref: '#/components/schemas/AddressInput'
              description: >-
                Billing/business address. Required when creating an account —
                a billable account carries its commercial address from the
                moment it exists. The address is geocoded via address
                validation.
            pricing:
              $ref: '#/components/schemas/AccountPricingRequest'
              description: >-
                Agreed monthly rates. Required — all three rates must be
                provided, and they are persisted atomically with the account
                (the account and its pricing commit together or not at all).

    UpdateAccountRequest:
      allOf:
        - $ref: '#/components/schemas/AccountRequest'
        - type: object
          properties:
            default_outbound_did:
              type: string
              description: ID of the phone number to use as the default outbound caller ID. Send null to clear.
            hold_music_clip:
              type: [string, 'null']
              description: ID of the audio clip played as hold music. Send null to clear (uses platform default).
            main_location:
              type: string
              description: |
                ID of the location to set as this account's main location. The
                location must belong to the account. The main location is the
                fallback for any user or phone number that doesn't have a
                specific location of its own (roaming). Cannot be cleared once
                set — swap to another location instead. Omit to leave
                unchanged.
            default_button_template:
              type: [string, 'null']
              description: |
                ID of a button template on the account to set as the default.
                Compatible deskphones and DECT handsets inherit it at creation.
                Send null to clear the default. Omit to leave unchanged.
            billing_address:
              oneOf:
                - $ref: '#/components/schemas/AddressInput'
                - type: 'null'
              description: >-
                Billing/business address. When present, the address is geocoded via address validation.
                Send null to clear. Omit to leave unchanged.

    UserRequest:
      type: object
      properties:
        name:
          $ref: '#/components/schemas/EntityName'
        email:
          type: string
          format: email
          description: Email address (optional)
          example: 'bob@spineline.dev'

    CreateUserRequest:
      $ref: '#/components/schemas/UserRequest'

    UpdateUserRequest:
      allOf:
        - $ref: '#/components/schemas/UserRequest'
        - type: object
          properties:
            outbound_caller_id_did:
              type: string
              description: ID of the phone number to use as this user's outbound caller ID, overriding the account default. Send null to clear.
            voicemail_pin:
              type: [string, 'null']
              description: Numeric PIN for remote voicemail access (4-10 digits). Send null to clear.
              example: '1234'
            do_not_disturb:
              type: boolean
              description: |
                Enable or disable server-side do-not-disturb for this user. Omit to leave unchanged. Must be a boolean — `null` is rejected.
              example: true
            config:
              type: object
              description: User configuration. Only provided fields are updated; omitted fields are left unchanged.
              properties:
                voicemail_notifications:
                  $ref: '#/components/schemas/VoicemailNotificationConfig'
                find_me_follow_me:
                  description: Find Me / Follow Me configuration. Send null to clear and revert to default behavior.
                  oneOf:
                    - $ref: '#/components/schemas/FMFM'
                    - type: 'null'
                mobile_push_wakeup:
                  type: boolean
                  description: |
                    Whether incoming calls should be held for a wake-up window when this user has no active
                    web/mobile calling session, so a push notification can wake their app to answer.

    UpdateVoicemailRequest:
      type: object
      properties:
        is_read:
          type: boolean
          description: Mark voicemail as read or unread

    CreateVoiceAppRequest:
      type: object
      properties:
        name:
          $ref: '#/components/schemas/EntityName'
        url:
          type: string
          format: uri
          description: HTTPS webhook URL for call control notifications
          example: 'https://ai.platform.example.com/calls'
      required:
        - name
        - url

    UpdateCallRequest:
      type: object
      description: Request to control an active call by sending actions
      properties:
        actions:
          type: array
          description: |
            List of actions to execute on the call. Actions are processed sequentially.
            Each action may block processing or allow it to continue based on its outcome.

            **Supported Actions:**

            ### attach
            Connect bidirectional audio streaming to a WebSocket URL.

            - `type`: `"attach"`
            - `url`: WebSocket URL (wss://) for bidirectional audio streaming

            **Processing behavior:** This action blocks processing while the WebSocket
            connection is active. When the WebSocket disconnects (either side closes),
            processing resumes with the next action.

            See [WebSocket API](/websocket-api) for the message protocol.

            ### transfer
            Blind transfer the call to an extension number, an E.164 phone
            number, or a SIP address.

            - `type`: `"transfer"`
            - `target`: Extension number, E.164 phone number, or `sip:` URI to
              transfer to. A `sip:` URI lets you route the call to an external
              SIP endpoint such as an AI voice agent
              (e.g. `sip:agent-7f3c@sip.example-ai.com`).
            - `mode`: `"blind"` (default) connects the caller and drops the originator immediately. Only `"blind"` is accepted in this release.

            **Processing behavior:** If the transfer target answers or the caller hangs up,
            processing stops. If the transfer cannot be completed (no answer, busy, rejected),
            processing continues with the next action.
          items:
            $ref: '#/components/schemas/CallAction'
          minItems: 1
      required:
        - actions

    CallAction:
      type: object
      oneOf:
        - $ref: '#/components/schemas/AttachAction'
        - $ref: '#/components/schemas/TransferAction'
      discriminator:
        propertyName: type
        mapping:
          attach: '#/components/schemas/AttachAction'
          transfer: '#/components/schemas/TransferAction'

    AttachAction:
      type: object
      properties:
        type:
          type: string
          enum: [attach]
          description: Action type
        url:
          type: string
          format: uri
          description: WebSocket URL (wss://) for bidirectional audio streaming
          example: 'wss://ai.platform.example.com/voice'
        metadata:
          type: object
          additionalProperties:
            type: string
          description: |
            Optional key/value pairs echoed on the media WebSocket `begin`
            message. Useful for correlating the stream with application-side
            state (agent ID, queue, trace ID, etc.) without a separate lookup.
          example:
            agent_id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
            queue: 'support'
      required:
        - type
        - url

    TransferAction:
      type: object
      properties:
        type:
          type: string
          enum: [transfer]
          description: Action type
        target:
          type: string
          description: |
            Where to transfer the call. One of: an extension number, an E.164
            phone number, or a `sip:` URI for an external SIP endpoint such as
            an AI voice agent (e.g. `sip:agent-7f3c@sip.example-ai.com`).
          example: '+14165550123'
        mode:
          type: string
          enum: [blind]
          description: >-
            Transfer mode. `blind` connects the caller to the target and drops
            the originator immediately. Defaults to blind when omitted.
          example: blind
      required:
        - type
        - target

    Listener:
      type: object
      description: |
        A listener streams real-time audio from an active call over a WebSocket connection.
        Audio flows unidirectionally from DialStack to your server. The call is not affected
        by the listener — both parties remain unaware that audio is being streamed.

        See the [WebSocket API](/websocket-api) for the listener message protocol.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          example: 'lstn_01h2xcejqtf2nbrexx3vqjhp50'
        call_id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          description: The active call being listened to
          example: 'call_01h2xcejqtf2nbrexx3vqjhp45'
        url:
          type: string
          format: uri
          description: WebSocket URL where audio is being streamed
          example: 'wss://your-server.example.com/audio'
        channel:
          type: string
          enum: [caller, callee, both]
          description: |
            Which audio channel(s) to stream:
            - `caller`: audio from the party that initiated the call
            - `callee`: audio from the party that received the call
            - `both`: both channels, delivered as separate tagged messages
          example: 'both'
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
      required:
        - id
        - call_id
        - url
        - channel
        - created_at

    SetAgentStatusRequest:
      type: object
      description: |
        Request to change a queue agent's availability state. `reason` is only
        valid when `status` is `paused`; sending it with any other status
        returns 400.
      properties:
        status:
          type: string
          enum: [available, paused, logged_out]
          description: |
            Target availability state:
            - `available` — the agent is logged in and eligible to receive calls
            - `paused` — the agent is logged in but temporarily unavailable
            - `logged_out` — the agent is not signed in to the queue
          example: 'paused'
        reason:
          type: string
          maxLength: 255
          description: |
            Optional free-form reason associated with a `paused` state
            (e.g., `break`, `lunch`, `training`). Only allowed when
            `status` is `paused`.
          example: 'break'
      required:
        - status

    AgentStatus:
      type: object
      description: |
        Current availability state for a queue agent. Timestamps are omitted
        when the corresponding attribute has not been set.
      properties:
        user:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          description: The agent (user) this status belongs to
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        status:
          type: string
          enum: [available, paused, logged_out]
          description: Current availability state
          example: 'paused'
        paused_at:
          type: string
          format: date-time
          description: When the agent entered the `paused` state, if applicable
        pause_reason:
          type: string
          description: Free-form reason for the current pause, if provided
          example: 'break'
        logged_in_at:
          type: string
          format: date-time
          description: When the agent first logged in to the queue
        in_call_since:
          type: string
          format: date-time
          description: When the agent's currently-active call started, if any
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - user
        - status
        - updated_at

    PresenceState:
      type: string
      enum: [available, on_call, offline]
      description: |
        Real-time reachability of a user, derived live from the phone system:

        - `available` — reachable on a device and not currently on a call.
        - `on_call` — currently on a call.
        - `offline` — a verified read that found the user reachable on no device.

        This is always a freshly verified value. When the underlying state
        cannot be confirmed, the request fails with `503` rather than returning
        a stale or guessed value — there is no `unknown`.
      example: 'available'

    WebRTCReachability:
      type: object
      description: |
        A user's reachability over WebRTC (a browser or in-app softphone)
        specifically, separate from the top-level `state`, which reflects any
        endpoint type. Use this to tell whether a user is genuinely reachable in
        their browser right now — e.g. to warn them their session has dropped
        before calls start skipping them.
      properties:
        state:
          allOf:
            - $ref: '#/components/schemas/PresenceState'
          description: |
            WebRTC-only reachability, using the same values as `state` but
            scoped to WebRTC:

            - `available` — at least one live WebRTC session, ready to ring.
            - `on_call` — currently on a call answered in a WebRTC session.
            - `offline` — no live WebRTC session (the user may still be
              reachable on another endpoint type; see the top-level `state`).

            Unlike the top-level `state`, this value does not carry a
            no-`unknown` guarantee in the strict sense: attributing a call to
            WebRTC specifically depends on a label the answering leg carries,
            and a call answered before that label was available reads as not-on-
            WebRTC rather than failing the request. This is deliberate — the
            top-level `state` stays fully verified rather than being degraded to
            a `503` over a WebRTC-only attribution gap. The condition is
            transient and clears as calls turn over.
        sessions:
          type: integer
          description: |
            Number of live WebRTC sessions for the user (for example, the same
            user signed in on two browser tabs counts as two).

            Treat this as eventually consistent in both directions, not as an
            exact live count. A session that reconnects (network change,
            sleep/wake) may be counted twice for a short window until the old
            connection times out, so this can briefly over-count; a
            just-established session may not be counted yet, so it can briefly
            under-count. In particular `sessions` can read `0` while `state` is
            `available` or `on_call` — `state` is the reachability answer, and
            `sessions` is a best-effort "how many" alongside it.
          example: 1
      required:
        - state
        - sessions

    UserPresence:
      type: object
      description: A user's current presence.
      properties:
        state:
          $ref: '#/components/schemas/PresenceState'
        notifiable:
          type: boolean
          description: |
            Whether the user can still be reached by waking a backgrounded or
            parked device when they are not currently registered. A separate
            axis from `state`: a user can be `offline` but `notifiable`.
          example: false
        do_not_disturb:
          type: boolean
          description: |
            Whether server-side do-not-disturb is enabled for this user. A
            separate axis from `state`: a user can be `available` but decline
            calls because `do_not_disturb` is on.
          example: false
        webrtc:
          $ref: '#/components/schemas/WebRTCReachability'
      required:
        - state
        - notifiable
        - do_not_disturb
        - webrtc

    UserPresenceItem:
      description: |
        A single user's presence within a bulk presence response: every
        `UserPresence` field plus the user it belongs to. Composed with `allOf`
        so the singleton and bulk shapes cannot drift as fields are added.
      allOf:
        - $ref: '#/components/schemas/UserPresence'
        - type: object
          properties:
            user:
              allOf:
                - $ref: '#/components/schemas/EntityId'
              description: The user this presence belongs to
              example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
          required:
            - user

    CreateListenerRequest:
      type: object
      description: Request to create a listener on an active call
      properties:
        url:
          type: string
          format: uri
          description: |
            Secure WebSocket URL where DialStack will stream audio. Must use
            the `wss://` scheme; insecure `ws://` URLs are rejected with 422.
          example: 'wss://your-server.example.com/audio'
        channel:
          type: string
          enum: [caller, callee, both]
          default: both
          description: |
            Which audio channel(s) to stream:
            - `caller`: audio from the party that initiated the call
            - `callee`: audio from the party that received the call
            - `both`: both channels, delivered as separate tagged messages
          example: 'both'
      required:
        - url

    UpdateVoiceAppRequest:
      type: object
      properties:
        name:
          $ref: '#/components/schemas/EntityName'
        url:
          type: string
          format: uri
          description: HTTPS webhook URL for call control notifications
          example: 'https://ai.platform.example.com/calls'
        status:
          type: string
          enum: [active, inactive]
          description: Voice app status

    FAQItem:
      type: object
      description: A question-answer pair for the AI agent's FAQ responses.
      properties:
        question:
          type: string
          description: The question the AI agent should recognize
          maxLength: 2000
          example: 'What are your business hours?'
        answer:
          type: string
          description: The answer the AI agent should provide
          maxLength: 2000
          example: 'We are open Monday through Friday, 9 AM to 5 PM.'
      required:
        - question
        - answer

    AIAgent:
      type: object
      description: |
        An AI agent provides an AI-powered voice receptionist for handling incoming calls.
        Each AI agent automatically manages a voice app and extension for call routing.
        Optional instructions and FAQ responses customize the agent's behavior at call time.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          example: 'aia_01h2xcejqtf2nbrexx3vqjhp60'
        name:
          allOf:
            - $ref: '#/components/schemas/EntityName'
          example: 'Front Desk Receptionist'
        voice_app:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          description: The managed voice app used for call routing
          example: 'va_01h2xcejqtf2nbrexx3vqjhp49'
        persona_name:
          type: [string, 'null']
          description: The name the AI agent uses to introduce itself on calls (e.g. "Tony"). Defaults to "the receptionist" if not set.
          maxLength: 255
          example: 'Tony'
        greeting_name:
          type: [string, 'null']
          description: The business name used in call greetings (e.g. "Thanks for calling [Greeting Name]"). When not set, the account name is used as a fallback.
          maxLength: 255
          example: 'Jones Family Dental'
        instructions:
          type: [string, 'null']
          description: Custom instructions for the AI agent's behavior
          maxLength: 10000
          example: 'You are the receptionist for Jones Family Dental. Be warm and professional.'
        faq_responses:
          type: array
          description: Pre-configured question-answer pairs
          items:
            $ref: '#/components/schemas/FAQItem'
        scheduling:
          $ref: '#/components/schemas/SchedulingConfig'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - name
        - voice_app
        - faq_responses
        - created_at
        - updated_at

    CreateAIAgentRequest:
      type: object
      description: Request body for creating an AI agent.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the AI agent
          example: 'Front Desk Receptionist'
        extension_number:
          type: string
          description: Extension number to assign for routing calls to this agent
          example: '200'
        persona_name:
          type: [string, 'null']
          description: The name the AI agent uses to introduce itself on calls
          maxLength: 255
          example: 'Tony'
        greeting_name:
          type: [string, 'null']
          description: The business name used in call greetings (e.g. "Thanks for calling [Greeting Name]"). When not set, the account name is used as a fallback.
          maxLength: 255
          example: 'Jones Family Dental'
        instructions:
          type: [string, 'null']
          description: Custom instructions for the AI agent's behavior
          maxLength: 10000
          example: 'You are the receptionist for Jones Family Dental.'
        faq_responses:
          type: array
          description: Pre-configured question-answer pairs
          items:
            $ref: '#/components/schemas/FAQItem'
        scheduling:
          $ref: '#/components/schemas/SchedulingConfig'
      required:
        - name

    UpdateAIAgentRequest:
      type: object
      description: Request body for updating an AI agent. All fields are optional.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the AI agent
          example: 'Updated Receptionist'
        persona_name:
          type: [string, 'null']
          description: The name the AI agent uses to introduce itself on calls
          maxLength: 255
          example: 'Tony'
        greeting_name:
          type: [string, 'null']
          description: The business name used in call greetings (e.g. "Thanks for calling [Greeting Name]"). When not set, the account name is used as a fallback.
          maxLength: 255
          example: 'Jones Family Dental'
        instructions:
          type: [string, 'null']
          description: Custom instructions for the AI agent's behavior
          maxLength: 10000
        faq_responses:
          type: array
          description: Pre-configured question-answer pairs (replaces all existing items)
          items:
            $ref: '#/components/schemas/FAQItem'
        scheduling:
          description: >-
            Scheduling configuration. Replaces the entire scheduling object
            (not merged field-by-field). Omit entirely to leave unchanged.
          $ref: '#/components/schemas/SchedulingConfig'

    SchedulingConfig:
      type: object
      description: >-
        Scheduling webhook configuration. Scheduling is on when webhook_url is
        provided, off when absent or empty. DialStack appends standard paths to
        the base URL for each operation.
        This is a replace-all object — send the complete configuration on every update.
      properties:
        webhook_url:
          type: string
          format: uri
          description: >-
            Base HTTPS URL for scheduling webhooks. DialStack appends paths
            for each operation: /customers/lookup, /availability/search, /bookings.
            Provide a URL to enable scheduling; omit or send empty to disable.
          maxLength: 2048

    CreateScheduleRequest:
      type: object
      description: Request body for creating a new schedule
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the schedule
          example: 'Business Hours'
        timezone:
          type: string
          description: IANA timezone for interpreting times (e.g., "America/New_York")
          example: 'America/New_York'
        ranges:
          type: array
          description: Weekly time ranges when the schedule is considered "open"
          items:
            $ref: '#/components/schemas/TimeRange'
          example:
            - day: 1
              start: '09:00'
              end: '17:00'
            - day: 2
              start: '09:00'
              end: '17:00'
        holidays:
          type: array
          description: Date ranges when the schedule is closed regardless of weekly ranges
          items:
            $ref: '#/components/schemas/DateRange'
      required:
        - name
        - ranges

    UpdateScheduleRequest:
      type: object
      description: |
        Request body for updating a schedule (all fields optional).
        Note: The `hold` field cannot be updated via this endpoint.
        Use POST /schedules/{schedule_id}/hold to set a hold, or
        DELETE /schedules/{schedule_id}/hold to clear it.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the schedule
          example: 'Updated Business Hours'
        timezone:
          type: string
          description: IANA timezone for interpreting times
          example: 'America/Los_Angeles'
        ranges:
          type: array
          description: Weekly time ranges when the schedule is considered "open"
          items:
            $ref: '#/components/schemas/TimeRange'
        holidays:
          type: array
          description: Date ranges when the schedule is closed regardless of weekly ranges
          items:
            $ref: '#/components/schemas/DateRange'

    HoldScheduleRequest:
      type: object
      description: |
        Request body for setting a hold on a schedule.
        The hold temporarily overrides the normal schedule calculation.
      properties:
        value:
          type: boolean
          description: Override value (true=force open, false=force closed)
          example: false
        until:
          type: string
          format: date-time
          description: |
            When the hold expires, as an absolute RFC3339 instant. If not provided,
            defaults to midnight (00:00:00) the next day in the schedule's (or
            account's) timezone.
          example: '2025-12-20T22:00:00Z'
      required:
        - value

    # Dial Plan Schemas

    DialPlan:
      type: object
      description: |
        A dial plan defines how calls are routed through a series of steps,
        represented as a flowchart. Each step is called a **node**.

        See the [Dial Plans Guide](/guides/dial-plans) for detailed documentation
        and examples.
      properties:
        id:
          type: string
          description: Unique identifier for the dial plan
          example: 'dp_01h2xcejqtf2nbrexx3vqjhp60'
        name:
          type: string
          description: Display name for the dial plan
          example: 'Main Line Routing'
        entry_node:
          type: string
          description: ID of the first node to execute (the implicit Start node connects here)
          example: 'check_hours'
        nodes:
          type: array
          description: All nodes in the dial plan graph
          items:
            $ref: '#/components/schemas/DialPlanNode'
        created_at:
          type: string
          format: date-time
          description: When the dial plan was created
          example: '2025-10-18T10:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: When the dial plan was last modified
          example: '2025-10-18T10:00:00Z'
        extensions:
          description: |
            List of extensions assigned to this dial plan. Only included when `expand[]=extensions` is requested.
          type: object
          properties:
            object:
              type: string
              enum: [list]
            data:
              type: array
              items:
                $ref: '#/components/schemas/Extension'
      required:
        - id
        - name
        - entry_node
        - nodes
        - created_at
        - updated_at

    DialPlanNode:
      type: object
      description: |
        A single step in a dial plan flowchart. Each node has a type that
        determines its behavior:

        - `schedule` — routes based on whether a schedule is currently open or closed; holidays optionally route to a dedicated branch
        - `internal_dial` — rings an internal target (user, ring group, call queue, dial plan, or shared voicemail) with timeout handling
        - `ring_all_users` — rings every user in the account at the same time
        - `external_dial` — dials an external phone number
        - `menu` — plays an audio prompt and routes based on DTMF input (IVR)
        - `audio_clip` — plays an audio clip then routes to the next node
          (the legacy value `sound_clip` is still accepted on Create/Update
          while in-flight clients catch up)
        - `voice_app` — hands the call off to a voice application via webhook
        - `hang_up` — ends the call with normal clearing

        The shape of `config` is determined by `type` — see the individual
        `*NodeConfig` schemas.
      properties:
        id:
          type: string
          description: Unique identifier for this node within the dial plan
          example: 'check_hours'
        type:
          type: string
          enum:
            [
              schedule,
              internal_dial,
              ring_all_users,
              external_dial,
              menu,
              audio_clip,
              sound_clip,
              voice_app,
              hang_up,
            ]
          description: |
            The node type. Determines which `*NodeConfig` schema the `config`
            field conforms to.
          example: 'schedule'
        position:
          $ref: '#/components/schemas/DialPlanPosition'
        config:
          description: |
            Type-specific configuration. The schema is selected by the `type`
            field:
            - `schedule` → `ScheduleNodeConfig`
            - `internal_dial` → `InternalDialNodeConfig`
            - `ring_all_users` → `RingAllUsersNodeConfig`
            - `external_dial` → `ExternalDialNodeConfig`
            - `menu` → `MenuNodeConfig`
            - `audio_clip` → `AudioClipNodeConfig` (legacy `sound_clip` is
              accepted with the same shape)
            - `voice_app` → `VoiceAppNodeConfig`
            - `hang_up` → `HangUpNodeConfig`

            Exit targets (the next node to execute) are embedded in the config
            (e.g. `open`/`closed` on schedule, `next` on dial nodes). A missing
            or null exit target terminates the call.
          oneOf:
            - $ref: '#/components/schemas/ScheduleNodeConfig'
            - $ref: '#/components/schemas/InternalDialNodeConfig'
            - $ref: '#/components/schemas/RingAllUsersNodeConfig'
            - $ref: '#/components/schemas/ExternalDialNodeConfig'
            - $ref: '#/components/schemas/MenuNodeConfig'
            - $ref: '#/components/schemas/AudioClipNodeConfig'
            - $ref: '#/components/schemas/VoiceAppNodeConfig'
            - $ref: '#/components/schemas/HangUpNodeConfig'
      required:
        - id
        - type
        - config

    DialPlanPosition:
      type: object
      description: |
        X/Y coordinates for visual rendering in the dial plan editor.
        Optional - if not provided, the node will be auto-positioned.
      properties:
        x:
          type: number
          format: float
          description: X coordinate in the visual editor
          example: 200
        y:
          type: number
          format: float
          description: Y coordinate in the visual editor
          example: 100
      required:
        - x
        - y

    ScheduleNodeConfig:
      type: object
      description: |
        Configuration for `schedule` nodes. Routes the call based on whether
        the referenced schedule is currently open, closed, or on a holiday.

        Holiday routing is per-node opt-in: when `holiday` is omitted, holiday
        state folds into the `closed` exit (the default for every dial plan).
        Set `holiday` to route holidays to a dedicated branch — for example,
        a "closed for holiday" announcement that's distinct from the regular
        closed-hours greeting.

        Exit targets are optional — if omitted or null, the call terminates.
      properties:
        schedule:
          type: string
          description: |
            ID of the schedule to check (returned by the Schedules API).
            May be empty on draft dial plans (terminates the call).
          example: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
        open:
          type: ['string', 'null']
          description: Node ID to route to when the schedule is open (null = terminate call)
          example: 'reception'
        closed:
          type: ['string', 'null']
          description: Node ID to route to when the schedule is closed (null = terminate call). Holiday state also routes here unless `holiday` is set.
          example: 'voicemail'
        holiday:
          type: ['string', 'null']
          description: |
            Node ID to route to when the schedule is on a holiday. When omitted
            or null, holiday state folds into the `closed` exit.
          example: 'holiday_announcement'

    InternalDialNodeConfig:
      type: object
      description: |
        Configuration for `internal_dial` nodes. Rings an internal target and
        handles no-answer / busy outcomes. If the call is answered the dial
        plan ends; otherwise it follows the `next` path (or terminates if
        `next` is missing or null).

        `target` may reference any of the following resources:

        - a **user** (returned by the Users API)
        - a **ring group** (returned by the Ring Groups API)
        - a **call queue** (returned by the Call Queues API)
        - a nested **dial plan** (returned by the Dial Plans API)
        - a **shared voicemail** (returned by the Voicemails API)
        - a **voice app** (deprecated; use the `voice_app` node type instead)

        The runtime dispatches on the kind of resource the `target` points
        to. For shared-voicemail targets, and for targets with `timeout: 0`,
        the call is delivered straight to voicemail and the dial plan ends —
        `next` is ignored in that case. For call-queue targets, the queue's
        own max-wait and timeout action take over once the caller is enqueued,
        and `next` is not used (except for the `timeout: 0` draft-skip).
      properties:
        target:
          type: string
          description: |
            ID of the internal target to dial. May be empty on draft dial
            plans (skips to `next`).
          example: 'user_01h2xcejqtf2nbrexx3vqjhp45'
        timeout:
          type: integer
          minimum: 0
          maximum: 300
          default: 30
          description: |
            Ring timeout in seconds (0–300). A value of `0` delivers the call
            directly to the target without ringing; when the target is a user
            or shared voicemail, this routes straight to voicemail.
          example: 30
        next:
          type: ['string', 'null']
          description: Node ID to route to on no-answer or busy (null = terminate call)
          example: 'voicemail'
      required:
        - timeout

    RingAllUsersNodeConfig:
      type: object
      description: |
        Configuration for `ring_all_users` nodes. Rings every user in the
        account at the same time and connects the first one to answer. If no
        user answers within `timeout`, the call follows the `next` path (or
        terminates if `next` is missing or null).
      properties:
        timeout:
          type: integer
          minimum: 1
          maximum: 300
          description: Ring timeout in seconds (1–300)
          example: 24
        next:
          type: ['string', 'null']
          description: Node ID to route to on no-answer (null = terminate call)
          example: 'voicemail'
      required:
        - timeout

    ExternalDialNodeConfig:
      type: object
      description: |
        Configuration for `external_dial` nodes. Dials an external phone
        number over the PSTN. If the call is answered the dial plan ends;
        otherwise it follows the `next` path (or terminates if `next` is
        missing or null).
      properties:
        phone_number:
          type: string
          pattern: '^(\+[1-9]\d{1,14})?$'
          description: |
            Destination phone number in E.164 format (e.g. `+14155551234`).
            May be empty on draft dial plans (skips to `next`).
          example: '+14155551234'
        timeout:
          type: integer
          minimum: 1
          maximum: 120
          description: Ring timeout in seconds (1–120)
          example: 60
        next:
          type: ['string', 'null']
          description: Node ID to route to on no-answer or busy (null = terminate call)
          example: 'voicemail'
      required:
        - timeout

    MenuNodeConfig:
      type: object
      description: |
        Configuration for `menu` nodes (IVR). Plays an audio prompt and waits
        for DTMF input. Each digit maps to a next node in the dial plan.

        If the caller presses an unmapped digit, the menu replays (or routes
        to `invalid_next_node` if connected). Any unconnected exit replays
        the menu prompt.
      properties:
        prompt_clip:
          type: string
          description: |
            ID of the audio clip to play as the menu prompt (returned by
            the Audio Clips API). May be empty on draft dial plans — when
            empty, playback is skipped and the menu waits for DTMF directly.
          example: 'aud_01h2xcejqtf2nbrexx3vqjhp50'
        timeout:
          type: integer
          minimum: 1
          maximum: 30
          description: Seconds to wait for DTMF input after the prompt finishes (1–30)
          example: 5
        options:
          type: array
          maxItems: 12
          description: DTMF digit-to-node mappings
          items:
            $ref: '#/components/schemas/MenuOption'
        timeout_next_node:
          type: ['string', 'null']
          description: Node ID to route to when no digit is pressed (null = replay menu prompt)
          example: 'voicemail'
        invalid_next_node:
          type: ['string', 'null']
          description: Node ID to route to when an unmapped digit is pressed (null = replay menu prompt)
          example: 'operator'
      required:
        - timeout

    MenuOption:
      type: object
      description: Maps a single DTMF digit to a destination node. Omit next_node to replay the menu prompt.
      properties:
        digit:
          type: string
          pattern: '^[0-9*#]$'
          description: 'DTMF digit: 0–9, *, or #'
          example: '1'
        next_node:
          type: ['string', 'null']
          description: Node ID to route to when this digit is pressed (null = replay menu prompt).
          example: 'sales'
        label:
          type: string
          maxLength: 20
          description: |
            Optional human-readable description shown next to the digit in the
            dial-plan editor (e.g., "Sales"). Display-only — the runtime ignores
            this field and routing is driven solely by `digit` and `next_node`.
          example: 'Sales'
      required:
        - digit

    AudioClipNodeConfig:
      type: object
      description: |
        Configuration for `audio_clip` nodes (also accepted under the legacy
        type name `sound_clip`). Plays an audio clip from the audio library
        then immediately routes to the next node.
      properties:
        clip:
          type: string
          description: |
            ID of the audio clip to play (returned by the Audio Clips API).
            May be empty on draft dial plans (skips to `next`).
          example: 'aud_01h2xcejqtf2nbrexx3vqjhp50'
        next:
          type: ['string', 'null']
          description: Node ID to route to after playing the clip (null = terminate call)
          example: 'main_menu'

    VoiceAppNodeConfig:
      type: object
      description: |
        Configuration for `voice_app` nodes. Hands the call off to a voice
        application via webhook dispatch. Voice apps do not ring; the dispatch
        is immediate.
      properties:
        voice_app:
          type: string
          description: |
            ID of the voice app to route to. May be empty on draft dial plans.
          example: 'va_01h2xcejqtf2nbrexx3vqjhp47'
        mode:
          type: string
          enum: [control, notify]
          default: control
          description: |
            Dispatch mode:
              - `control`: hands the call to the voice app. The webhook takes
                ownership and decides what happens next via the Update Call API.
              - `notify`: fires a `call.notify` webhook for visibility (BYO
                Observer / transcription / analytics) while the dial plan
                continues routing via `next`. The call is never answered or
                held by this node.

            `notify` is rejected (422) when the referenced voice app is backed
            by an AI agent — those agents control calls and cannot function as
            passive observers. Use a plain webhook voice app for notify nodes.
          example: 'control'
        next:
          type: ['string', 'null']
          description: |
            In `notify` mode, the always-taken continuation. In `control` mode,
            the fallback on dispatch failure (null = terminate call).
          example: 'voicemail'

    HangUpNodeConfig:
      type: object
      description: |
        Configuration for `hang_up` nodes. Terminates the call with normal
        clearing. Zero-config; the canonical wire form is the empty object.
      additionalProperties: false

    CreateDialPlanRequest:
      type: object
      description: Request body for creating a new dial plan
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the dial plan
          example: 'Main Line Routing'
        entry_node:
          type: string
          description: ID of the first node to execute (optional for draft dial plans)
          example: 'check_hours'
        nodes:
          type: array
          description: All nodes in the dial plan graph
          items:
            $ref: '#/components/schemas/DialPlanNode'
      required:
        - name

    UpdateDialPlanRequest:
      type: object
      description: |
        Request body for updating a dial plan. All fields are optional.
        When `nodes` is provided, it fully replaces all nodes (not a partial node update).
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the dial plan
          example: 'Updated Main Line Routing'
        entry_node:
          type: string
          description: ID of the first node to execute
          example: 'check_hours'
        nodes:
          type: array
          description: All nodes in the dial plan graph (full replacement)
          items:
            $ref: '#/components/schemas/DialPlanNode'

    # Ring Group Schemas

    RingGroup:
      type: object
      description: A ring group for parallel dialing (call forking)
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Ring group identifier
              example: 'rg_01h2xcejqtf2nbrexx3vqjhp51'
        name:
          type: string
          description: Display name
          example: 'Sales Team'
        timeout_seconds:
          type: integer
          minimum: 5
          maximum: 300
          description: How long to ring members before giving up (seconds)
          example: 30
        ignore_forwarding:
          type: boolean
          description: When true, ignores SIP 302 redirects (call forwarding) from devices
          example: false
        confirm_external:
          type: boolean
          description: When true, requires external (phone number) members to press a key before being connected
          example: false
        timeout_action:
          type: [string, 'null']
          enum: [ring_user, voicemail, queue, null]
          description: |
            Action to take when the ring group times out:
            * `ring_user` — ring the fallback user.
            * `voicemail` — send to voicemail (the user's box or a shared box, determined by `timeout_target`).
            * `queue` — overflow the caller into a queue.
          example: 'voicemail'
        timeout_target:
          type: [string, 'null']
          description: |
            ID of the timeout target — a user, shared voicemail box, or queue, paired with `timeout_action`.
            Required when `timeout_action` is set.
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        extensions:
          description: |
            List of extensions assigned to this ring group. Only included when `expand[]=extensions` is requested.
          type: object
          properties:
            object:
              type: string
              enum: [list]
            data:
              type: array
              items:
                $ref: '#/components/schemas/Extension'
        members:
          type: array
          items:
            $ref: '#/components/schemas/RingGroupMember'
          description: List of members in this ring group
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - name
        - timeout_seconds
        - ignore_forwarding
        - confirm_external
        - members
        - created_at
        - updated_at

    RingGroupMember:
      type: object
      description: A member of a ring group (either an extension or phone number)
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Member identifier
              example: 'rgm_01h2xcejqtf2nbrexx3vqjhp52'
        ring_group:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Parent ring group
        extension:
          type: [string, 'null']
          description: ID of a routing target
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        phone_number:
          type: [string, 'null']
          description: Dial string (E.164, local number, extension, 911, etc.)
          example: '+14155551234'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
      required:
        - id
        - ring_group
        - created_at

    CreateRingGroupRequest:
      type: object
      description: Request body for creating a ring group
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the ring group
          example: 'Sales Team'
        timeout_seconds:
          type: integer
          minimum: 5
          maximum: 300
          description: How long to ring members (defaults to 20)
          example: 30
        ignore_forwarding:
          type: boolean
          description: Ignore SIP 302 redirects from devices
          example: false
        confirm_external:
          type: boolean
          description: Require external (phone number) members to press a key before being connected
          example: false
        timeout_action:
          type: string
          enum: [ring_user, voicemail, queue]
          description: |
            Action to take when the ring group times out:
            * `ring_user` — ring the fallback user.
            * `voicemail` — send to voicemail (the user's box or a shared box, determined by `timeout_target`).
            * `queue` — overflow the caller into a queue.
          example: 'voicemail'
        timeout_target:
          type: string
          description: |
            ID of the timeout target — a user, shared voicemail box, or queue, paired with `timeout_action`.
            Required when `timeout_action` is set.
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
      required:
        - name

    UpdateRingGroupRequest:
      type: object
      description: |
        Request body for updating a ring group (all fields optional).
        To clear the timeout behavior, send `timeout_action` and `timeout_target` as `null`.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: New display name
          example: 'Support Team'
        timeout_seconds:
          type: integer
          minimum: 5
          maximum: 300
          description: New timeout value
          example: 45
        ignore_forwarding:
          type: boolean
          description: New ignore_forwarding setting
          example: true
        confirm_external:
          type: boolean
          description: New confirm_external setting
          example: true
        timeout_action:
          type: [string, 'null']
          enum: [ring_user, voicemail, queue, null]
          description: |
            Action to take when the ring group times out:
            * `ring_user` — ring the fallback user.
            * `voicemail` — send to voicemail (the user's box or a shared box, determined by `timeout_target`).
            * `queue` — overflow the caller into a queue.

            Send `null` together with a `null` `timeout_target` to clear the timeout configuration.
          example: 'voicemail'
        timeout_target:
          type: [string, 'null']
          description: |
            ID of the timeout target — a user, shared voicemail box, or queue, paired with `timeout_action`.
            Required when `timeout_action` is set.
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'

    AddRingGroupMemberRequest:
      type: object
      description: Request body for adding a member (exactly one of extension or phone_number required)
      properties:
        extension:
          type: string
          description: ID of a routing target
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        phone_number:
          type: string
          description: Dial string (E.164, local number, extension, 911, etc.)
          example: '+14155551234'

    # Call Queue Schemas

    QueueStrategy:
      type: string
      enum: [ringall, linear, rrmemory, leastrecent, fewestcalls, random, wrandom]
      description: |
        Dispatch strategy for selecting which agent to ring next.
        * `ringall` — ring every available agent in parallel; first to answer wins.
        * `linear` — ring agents in `position` order, lowest first.
        * `rrmemory` — round-robin with memory; resumes after the last agent dialed.
        * `leastrecent` — ring the agent who has not taken a call for the longest time.
        * `fewestcalls` — ring the agent with the fewest calls taken in the current window.
        * `random` — pick uniformly at random from available agents.
        * `wrandom` — weighted random; lower `penalty` is more likely to be picked.

    QueueEmptyPolicy:
      type: string
      enum: [strict, no_agents_online, no_agents_available]
      description: |
        Policy used by `join_empty` and `leave_when_empty`.
        * `strict` — apply the policy whenever no agents exist on the queue.
        * `no_agents_online` — apply when no agents are logged in.
        * `no_agents_available` — apply when no agents are available (logged in and not paused / in-call / wrapping up).

    QueueRingUserTimeout:
      type: object
      description: Ring a fallback user when `timeout_seconds` elapses.
      properties:
        type:
          type: string
          enum: [ring_user]
        user:
          type: string
          description: ID of the user to ring.
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
      required:
        - type
        - user

    QueueVoicemailTimeout:
      type: object
      description: |
        Send the caller to voicemail when `timeout_seconds` elapses. The
        target may be either a user (delivers to their personal voicemail)
        or a shared voicemail box.
      properties:
        type:
          type: string
          enum: [voicemail]
        voicemail:
          type: string
          description: ID of the user or shared voicemail box that receives the message.
          example: 'svm_01h2xcejqtf2nbrexx3vqjhp60'
      required:
        - type
        - voicemail

    QueueTimeout:
      description: |
        Discriminated configuration for what happens when a caller waits past
        `timeout_seconds`. Exactly one target field is set, matching `type`.

        Send `null` (instead of an object) on update to clear the timeout
        configuration.
      oneOf:
        - $ref: '#/components/schemas/QueueRingUserTimeout'
        - $ref: '#/components/schemas/QueueVoicemailTimeout'
      discriminator:
        propertyName: type
        mapping:
          ring_user: '#/components/schemas/QueueRingUserTimeout'
          voicemail: '#/components/schemas/QueueVoicemailTimeout'

    QueueCallbackConfig:
      type: object
      description: |
        Press-1 callback configuration on a queue. Setting the `callback`
        field on a Queue to this object enables callbacks; setting it to
        `null` disables them. `offer_after_seconds` controls the delay
        before the first press-1 offer; `outbound_did` is the optional
        per-queue outbound caller ID for callback originates (when null,
        the dispatcher falls back to the captured inbound DID, then to the
        account default outbound DID).
      properties:
        offer_after_seconds:
          type: integer
          minimum: 0
          maximum: 3600
          description: Delay before the first callback offer (seconds; 0 = immediate)
          example: 60
        outbound_did:
          type: [string, 'null']
          description: Per-queue callback outbound caller ID; null = fall back to inbound DID then account default
          example: 'did_01h2xcejqtf2nbrexx3vqjhp44'
      required:
        - offer_after_seconds
        - outbound_did

    QueueCallbackConfigInput:
      type: object
      description: |
        Request shape for configuring press-1 callbacks on a queue. Providing
        the `callback` object enables callbacks; passing `callback: null`
        disables them. Fields inside the object are optional: on create,
        `offer_after_seconds` defaults to 60 and omitted `outbound_did`
        means use the captured inbound DID, then the account default outbound
        DID. On update, omitted fields leave the current values unchanged, while
        explicit `outbound_did: null` clears the per-queue outbound caller ID.
      properties:
        offer_after_seconds:
          type: integer
          minimum: 0
          maximum: 3600
          description: Delay before the first callback offer (seconds; 0 = immediate)
          example: 60
        outbound_did:
          type: [string, 'null']
          description: Per-queue callback outbound caller ID; null = fall back to inbound DID then account default
          example: 'did_01h2xcejqtf2nbrexx3vqjhp44'

    QueueAnnouncementsConfig:
      type: object
      description: |
        Periodic "you are caller number N" announcement configuration on a
        queue. Setting the `announcements` field on a Queue to this object
        enables announcements; setting it to `null` disables them.
      properties:
        frequency_seconds:
          type: integer
          minimum: 10
          maximum: 600
          description: Cadence for position announcements (seconds).
          example: 30
      required:
        - frequency_seconds

    QueueAnnouncementsConfigInput:
      type: object
      description: |
        Request shape for configuring position announcements on a queue.
        Providing the `announcements` object enables announcements; passing
        `announcements: null` disables them. `frequency_seconds` is optional:
        on create it defaults to 30, on update an omitted field leaves the
        current value unchanged.
      properties:
        frequency_seconds:
          type: integer
          minimum: 10
          maximum: 600
          description: Cadence for position announcements (seconds).
          example: 30

    EmbeddedQueueMembers:
      type: object
      description: |
        Sub-collection of queue members embedded on the parent queue.
        Capped at 10 entries; use `next_page_url` (or `GET /v1/queues/{queue_id}/members`)
        to page through the rest.
      properties:
        object:
          $ref: '#/components/schemas/ListObject'
        url:
          $ref: '#/components/schemas/ListUrl'
        next_page_url:
          $ref: '#/components/schemas/NextPageUrl'
        previous_page_url:
          $ref: '#/components/schemas/PreviousPageUrl'
        data:
          type: array
          items:
            $ref: '#/components/schemas/QueueMember'
        total_count:
          type: integer
          description: Total number of members on the queue (not just the embedded page).
          example: 12
      required:
        - object
        - url
        - data
        - total_count

    Queue:
      type: object
      description: |
        A call queue. Callers reaching the queue are placed on hold and dispatched
        to a member (agent) according to the configured `strategy`. Members are
        users; an agent's availability is tracked separately via
        `GET /v1/users/{user_id}/queue-agent`.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Queue identifier
              example: 'qu_01h2xcejqtf2nbrexx3vqjhp61'
        name:
          type: string
          description: Display name
          example: 'Support'
        strategy:
          $ref: '#/components/schemas/QueueStrategy'
        timeout_seconds:
          type: integer
          minimum: 0
          maximum: 3600
          description: |
            How long a caller waits before the `timeout` action fires. `0` means
            no timeout — callers wait until an agent answers or they hang up.
          example: 120
        wrap_up_seconds:
          type: integer
          minimum: 0
          maximum: 600
          description: |
            Cool-down per agent after each call ends, during which the agent is
            not eligible for dispatch. `0` disables wrap-up.
          example: 15
        announcements:
          oneOf:
            - $ref: '#/components/schemas/QueueAnnouncementsConfig'
            - type: 'null'
          description: Position-announcement configuration; null when announcements are disabled.
        callback:
          oneOf:
            - $ref: '#/components/schemas/QueueCallbackConfig'
            - type: 'null'
          description: Press-1 callback configuration; null when callbacks are disabled
        timeout:
          oneOf:
            - $ref: '#/components/schemas/QueueTimeout'
            - type: 'null'
          description: |
            Fallback action when `timeout_seconds` elapses. `null` means
            "hang up the caller" — the timer still fires when
            `timeout_seconds > 0`. To opt out of any timeout entirely
            (callers wait until an agent picks up or they hang up), set
            `timeout_seconds` to `0`; `timeout` is then irrelevant.
        max_queue_length:
          type: integer
          minimum: 0
          description: |
            Maximum number of callers waiting at once. `0` means unlimited.
            When the limit is reached, external callers hear a "queue full"
            announcement and are hung up; internal callers receive `486 Busy`.
          example: 50
        join_empty:
          $ref: '#/components/schemas/QueueEmptyPolicy'
        leave_when_empty:
          $ref: '#/components/schemas/QueueEmptyPolicy'
        members:
          $ref: '#/components/schemas/EmbeddedQueueMembers'
        extensions:
          description: |
            Extensions assigned to this queue. Only included when `expand[]=extensions`
            is requested.
          type: object
          properties:
            object:
              type: string
              enum: [list]
            data:
              type: array
              items:
                $ref: '#/components/schemas/Extension'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - name
        - strategy
        - timeout_seconds
        - wrap_up_seconds
        - announcements
        - callback
        - timeout
        - max_queue_length
        - join_empty
        - leave_when_empty
        - members
        - created_at
        - updated_at

    QueueMember:
      type: object
      description: A user agent assigned to a call queue.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Queue member identifier
              example: 'qum_01h2xcejqtf2nbrexx3vqjhp62'
        queue:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Parent queue
              example: 'qu_01h2xcejqtf2nbrexx3vqjhp61'
        user:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: User who acts as the agent
              example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        penalty:
          type: integer
          minimum: 0
          description: |
            Strategy hint. Lower-penalty agents are preferred by `linear`,
            `wrandom`, and tie-breakers in other strategies. Defaults to `0`.
          example: 0
        position:
          type: integer
          minimum: 1
          description: |
            1-based ordering within the queue, used by `linear` and `rrmemory`
            for deterministic rotation. Members default to the next free
            position; pass an explicit `position` to insert mid-list.
          example: 3
        created_at:
          $ref: '#/components/schemas/CreatedAt'
      required:
        - id
        - queue
        - user
        - penalty
        - position
        - created_at

    CreateQueueRequest:
      type: object
      description: Request body for creating a queue.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the queue.
          example: 'Support'
        strategy:
          allOf:
            - $ref: '#/components/schemas/QueueStrategy'
          description: Dispatch strategy. Defaults to `ringall`.
        timeout_seconds:
          type: integer
          minimum: 0
          maximum: 3600
          default: 300
          description: |
            Caller wait limit before `timeout` fires. Defaults to `300`
            (5 minutes) so a caller can't sit forever if every eligible
            agent goes offline. When the timer fires, `timeout` decides
            what happens — `null`/omitted means hang up. Pass `0` to opt
            out of any timeout entirely (callers wait indefinitely until
            an agent picks up or they hang up).
          example: 120
        wrap_up_seconds:
          type: integer
          minimum: 0
          maximum: 600
          description: Per-agent cool-down after each call. Defaults to `0`.
          example: 15
        announcements:
          oneOf:
            - $ref: '#/components/schemas/QueueAnnouncementsConfigInput'
            - type: 'null'
          description: |
            Position-announcement config. Pass an object (with any subset of
            fields) to enable announcements; omit or pass null to disable.
        callback:
          oneOf:
            - $ref: '#/components/schemas/QueueCallbackConfigInput'
            - type: 'null'
          description: |
            Press-1 callback config. Pass an object (with any subset of
            fields) to enable callbacks; omit or pass null to disable.
        timeout:
          oneOf:
            - $ref: '#/components/schemas/QueueTimeout'
            - type: 'null'
          description: |
            Fallback action when `timeout_seconds` elapses. Omitting or
            sending `null` means "hang up" — the timer still fires
            because `timeout_seconds` defaults to `300`. To opt out of
            any timer, set `timeout_seconds: 0`; `timeout` is then
            ignored.
        max_queue_length:
          type: integer
          minimum: 0
          description: Maximum number of waiting callers. `0` = unlimited (default).
          example: 50
        join_empty:
          $ref: '#/components/schemas/QueueEmptyPolicy'
        leave_when_empty:
          $ref: '#/components/schemas/QueueEmptyPolicy'
      required:
        - name

    UpdateQueueRequest:
      type: object
      description: |
        Request body for updating a queue. All fields are optional.

        Sending `{"callback": null}` disables callbacks; sending
        `{"callback": {...}}` enables and applies the provided fields. Within
        the callback object, omitting `outbound_did` leaves the existing
        DID untouched, while sending an explicit `null` clears it. The same
        pattern applies to `timeout` at the top level — send `null` to clear,
        omit to leave unchanged.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        strategy:
          $ref: '#/components/schemas/QueueStrategy'
        timeout_seconds:
          type: integer
          minimum: 0
          maximum: 3600
        wrap_up_seconds:
          type: integer
          minimum: 0
          maximum: 600
        announcements:
          oneOf:
            - $ref: '#/components/schemas/QueueAnnouncementsConfigInput'
            - type: 'null'
          description: |
            Position-announcement config. Pass null to disable; pass an object
            to enable/update. Omitted leaves the existing value untouched.
        callback:
          oneOf:
            - $ref: '#/components/schemas/QueueCallbackConfigInput'
            - type: 'null'
          description: |
            Press-1 callback config. Pass null to disable; pass an object to
            enable/update. Omitted leaves the existing value untouched.
        timeout:
          oneOf:
            - $ref: '#/components/schemas/QueueTimeout'
            - type: 'null'
          description: Send `null` to clear timeout behavior; omit to leave unchanged.
        max_queue_length:
          type: integer
          minimum: 0
        join_empty:
          $ref: '#/components/schemas/QueueEmptyPolicy'
        leave_when_empty:
          $ref: '#/components/schemas/QueueEmptyPolicy'

    AddQueueMemberRequest:
      type: object
      description: Request body for adding a user agent to a queue.
      properties:
        user:
          type: string
          description: User TypeID (`user_...`) of the agent to add.
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        penalty:
          type: integer
          minimum: 0
          description: Strategy hint. Defaults to `0`.
          example: 0
        position:
          type: integer
          minimum: 1
          description: 1-based insertion position. Defaults to the end of the queue.
          example: 3
      required:
        - user

    # Shared Voicemail Box Schemas

    File:
      type: object
      description: |
        An uploaded file scoped to an account. Returned inline when a resource
        that references a file is expanded (e.g. a fax under `expand[]=file`).
      properties:
        object:
          type: string
          enum: [file]
          description: String representing the object's type.
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: File identifier.
        purpose:
          type: string
          description: What the file is used for (e.g. `fax_source`).
          example: fax_source
        filename:
          type: [string, 'null']
          description: Original uploaded filename, if known.
          example: invoice.pdf
        type:
          type: [string, 'null']
          description: Short type derived from the MIME type (e.g. `pdf`), or null when unrecognized.
          example: pdf
        mime_type:
          type: string
          description: Detected or declared content type.
          example: application/pdf
        size:
          type: integer
          description: File size in bytes.
          example: 51234
        url:
          type: [string, 'null']
          description: |
            A time-limited signed URL for the file's bytes, populated at
            response time only. Null unless the file was returned via an expand.
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - object
        - id
        - purpose
        - filename
        - type
        - mime_type
        - size
        - url
        - created_at
        - updated_at

    Fax:
      type: object
      description: |
        A fax record. Outbound faxes progress `pending` → `delivered`/`failed`;
        inbound faxes are stored as `received`. The fax document is the `file`,
        reachable as a signed URL via `expand[]=file`.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Fax identifier
        direction:
          type: string
          enum: [inbound, outbound]
          description: Whether the fax was sent or received.
        status:
          type: string
          enum: [pending, delivered, failed, received]
          description: Lifecycle state of the fax.
        file:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - $ref: '#/components/schemas/File'
            - type: 'null'
          description: |
            The fax document. The file's id by default; the full File object
            carrying a time-limited signed `url` when `expand[]=file` is
            requested; null until the document exists (an inbound row before
            receipt, or a failed fax).
        did:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - $ref: '#/components/schemas/DIDSummary'
          description: |
            Your own number on the fax — the source DID for outbound, the
            terminating DID for inbound. The DID's id by default; the
            DIDSummary object when `expand[]=did` is requested.
        from_number:
          type: [string, 'null']
          description: Sender's number in E.164. Null on inbound when the caller ID was withheld.
          example: '+14155551234'
        to_number:
          type: [string, 'null']
          description: Recipient's number in E.164.
          example: '+14155559876'
        call_id:
          type: [string, 'null']
          description: ID of the call leg that carried the fax, if any.
        pages:
          type: [integer, 'null']
          description: Number of pages transmitted, once known.
          example: 3
        source_pages:
          type: [integer, 'null']
          description: |
            Page count of the source document (outbound only). Null on inbound
            and until known. Pair with `pages` to show progress while sending,
            e.g. "15 of 30 sent".
          example: 30
        transport:
          type: [string, 'null']
          enum: [t38, g711, null]
          description: Transport the leg negotiated. Null while pending or if the fax failed before negotiation.
        error_code:
          type: [string, 'null']
          description: Error code when the fax failed.
        attempts:
          type: integer
          description: Number of delivery attempts made.
          example: 1
        read_at:
          type: [string, 'null']
          format: date-time
          description: |
            When the fax was marked read, or null if unread. Read state is the
            null-ness of this field; applies to inbound (received) faxes.
        submitted_at:
          type: [string, 'null']
          format: date-time
          description: When the fax was submitted to the carrier.
        completed_at:
          type: [string, 'null']
          format: date-time
          description: When the fax reached a terminal state.
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - direction
        - status
        - file
        - did
        - from_number
        - to_number
        - call_id
        - pages
        - source_pages
        - transport
        - error_code
        - attempts
        - read_at
        - submitted_at
        - completed_at
        - created_at
        - updated_at

    AudioClip:
      type: object
      description: An uploaded audio file for hold music, IVR prompts, or other audio playback
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Audio clip identifier
        name:
          type: string
          description: Display name (unique per account)
          example: 'Hold Music'
        url:
          type: string
          description: Signed URL for downloading the audio file (expires after 5 minutes)
          example: 'https://cdn.example.com/audio-clips/clip.wav?Expires=...'
        mime_type:
          type: string
          description: MIME type of the stored audio file
          example: 'audio/wav'
        duration_ms:
          type: integer
          description: Duration of the audio clip in milliseconds
          example: 30000
        size_bytes:
          type: integer
          format: int64
          description: Size of the stored audio file in bytes
          example: 240000
        source_sha256:
          type: [string, 'null']
          description: SHA-256 hash of the original uploaded file (before transcoding)
          example: '113a95dbbfd24492fb4e527b58d64aece235d6a5057ea671dbf2fe980a9cf6a3'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - name
        - url
        - mime_type
        - duration_ms
        - size_bytes
        - created_at
        - updated_at

    UpdateAudioClipRequest:
      type: object
      description: Request body for updating an audio clip (all fields optional)
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: New display name
          example: 'Updated Hold Music'

    SharedVoicemailBox:
      type: object
      description: A shared voicemail box for team or departmental voicemail
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Shared voicemail box identifier
              example: 'svm_01h2xcejqtf2nbrexx3vqjhp60'
        name:
          type: string
          description: Display name
          example: 'Sales Voicemail'
        email:
          type: [string, 'null']
          description: Email address to send voicemail notifications to
          example: 'sales@example.com'
        pin:
          type: [string, 'null']
          description: PIN code for accessing the voicemail box (4-10 digits)
          example: '1234'
        email_attach_audio:
          type: boolean
          description: Whether to attach the audio recording to notification emails
          example: true
        email_include_summary:
          type: boolean
          description: Whether to include a summary in notification emails
          example: true
        email_include_transcript:
          type: boolean
          description: Whether to include a transcript in notification emails
          example: true
        delete_after_email:
          type: boolean
          description: Whether to delete the voicemail after sending the email notification
          example: true
        extensions:
          description: |
            List of extensions assigned to this shared voicemail box. Only included when `expand[]=extensions` is requested.
          type: object
          properties:
            object:
              type: string
              enum: [list]
            data:
              type: array
              items:
                $ref: '#/components/schemas/Extension'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - name
        - email_attach_audio
        - email_include_summary
        - email_include_transcript
        - delete_after_email
        - created_at
        - updated_at

    CreateSharedVoicemailBoxRequest:
      type: object
      description: Request body for creating a shared voicemail box
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Display name for the shared voicemail box
          example: 'Sales Voicemail'
        email:
          type: string
          description: Email address to send voicemail notifications to
          example: 'sales@example.com'
        pin:
          type: string
          pattern: '^\d{4,10}$'
          description: PIN code for accessing the voicemail box (4-10 digits)
          example: '1234'
        email_attach_audio:
          type: boolean
          description: Whether to attach the audio recording to notification emails (defaults to true)
          example: true
        email_include_summary:
          type: boolean
          description: Whether to include a summary in notification emails (defaults to true)
          example: true
        email_include_transcript:
          type: boolean
          description: Whether to include a transcript in notification emails (defaults to true)
          example: true
        delete_after_email:
          type: boolean
          description: Whether to delete the voicemail after sending the email notification (defaults to true)
          example: true
      required:
        - name

    UpdateSharedVoicemailBoxRequest:
      type: object
      description: |
        Request body for updating a shared voicemail box (all fields optional).
        To clear optional fields, send them as `null`.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: New display name
          example: 'Support Voicemail'
        email:
          type: [string, 'null']
          description: Email address to send voicemail notifications to
          example: 'support@example.com'
        pin:
          type: [string, 'null']
          pattern: '^\d{4,10}$'
          description: PIN code for accessing the voicemail box (4-10 digits)
          example: '5678'
        email_attach_audio:
          type: boolean
          description: Whether to attach the audio recording to notification emails
          example: false
        email_include_summary:
          type: boolean
          description: Whether to include a summary in notification emails
          example: false
        email_include_transcript:
          type: boolean
          description: Whether to include a transcript in notification emails
          example: false
        delete_after_email:
          type: boolean
          description: Whether to delete the voicemail after sending the email notification
          example: false

    # Device User Assignment Schemas

    AssignUserToDeviceRequest:
      type: object
      description: Request body for assigning a user to a device
      properties:
        user:
          type: string
          description: The user to assign to the deskphone
          example: '01h2xcejqtf2nbrexx3vqjhp42'
      required:
        - user

    DeviceUserAssignment:
      type: object
      description: A user assigned to a device
      properties:
        user:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: The assigned user
              example: '01h2xcejqtf2nbrexx3vqjhp42'
        device:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: The device
              example: '01h2xcejqtf2nbrexx3vqjhp50'
        line_number:
          type: integer
          minimum: 1
          maximum: 24
          description: Physical line key number assigned to this user
          example: 1
        created_at:
          $ref: '#/components/schemas/CreatedAt'
      required:
        - user
        - device
        - created_at

    # Device Line Schemas

    DeviceLine:
      type: object
      description: A SIP line assignment on a provisioned device, mapping a line key to a SIP endpoint
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Device line identifier
              example: 'dln_01h2xcejqtf2nbrexx3vqjhp55'
        device:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Parent device
              example: 'dev_01h2xcejqtf2nbrexx3vqjhp50'
        endpoint:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: The SIP endpoint assigned to this line
              example: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
        line_number:
          type: integer
          minimum: 1
          maximum: 24
          description: Physical line key number on the device
          example: 1
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - device
        - endpoint
        - line_number
        - created_at
        - updated_at

    CreateDeviceLineRequest:
      type: object
      description: Request body for creating a device line
      properties:
        endpoint:
          type: string
          description: ID of the SIP endpoint to assign to this line
          example: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
      required:
        - endpoint

    UpdateDeviceLineRequest:
      type: object
      description: Request body for reassigning a device line to a different endpoint
      properties:
        endpoint:
          type: string
          description: ID of the new SIP endpoint to assign to this line
          example: 'ep_01h2xcejqtf2nbrexx3vqjhp44'
      required:
        - endpoint

    # Programmable Button Schemas

    ButtonType:
      type: string
      enum:
        - line
        - blf_extension
        - blf_park
        - blf_voicemail_shared
        - blf_queue_agent
        - blf_queue_depth
        - speed_dial
        - dtmf
        - voicemail
        - url
        - multicast
        - conference
        - transfer
        - forward
        - park
        - intercom
        - dnd
        - record_toggle
      description: Programmable key behavior discriminator.

    ButtonTarget:
      type: object
      description: |
        Type-specific button payload. Examples include `{"user":"user_..."}`,
        `{"queue":"qu_..."}`, `{"destination":"+14155551234"}`,
        `{"slot":1}`, or `{}` depending on `type`.
      additionalProperties: true
      example:
        user: 'user_01h2xcejqtf2nbrexx3vqjhp42'

    ButtonCompatibilityVerdict:
      type: object
      properties:
        supported:
          type: boolean
          description: Whether this device can render the button.
        reason:
          type: string
          description: Stable reason when unsupported.
          enum:
            [
              vendor_does_not_support_type,
              position_out_of_range_for_model,
              device_has_no_owning_user,
            ]
      required:
        - supported

    ButtonCompatibilitySummary:
      type: object
      properties:
        device:
          type: object
          properties:
            vendor:
              type: string
              example: snom
            model:
              type: string
              example: D785
            kind:
              type: string
              enum: [deskphone, dect_base, dect_handset]
            max_position:
              type: integer
              description: Highest programmable key position known for this device model.
          required:
            - vendor
            - model
            - kind
            - max_position
        supported_count:
          type: integer
          description: Number of buttons supported by this device.
        unsupported:
          type: array
          items:
            type: object
            properties:
              template_button:
                type: string
                example: 'btn_01h2xcejqtf2nbrexx3vqjhp71'
              override:
                type: string
                example: 'devbtn_01h2xcejqtf2nbrexx3vqjhp72'
              position:
                type: integer
                minimum: 1
              type:
                $ref: '#/components/schemas/ButtonType'
              reason:
                type: string
                enum:
                  [
                    vendor_does_not_support_type,
                    position_out_of_range_for_model,
                    device_has_no_owning_user,
                  ]
            required:
              - position
              - type
              - reason
      required:
        - device
        - supported_count
        - unsupported

    ButtonTemplate:
      type: object
      description: Reusable account-scoped programmable-key template.
      properties:
        id:
          type: string
          example: 'btpl_01h2xcejqtf2nbrexx3vqjhp70'
        name:
          type: string
          example: Front Desk
        description:
          type: [string, 'null']
          example: Shared front desk programmable keys
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - name
        - created_at
        - updated_at

    TemplateButton:
      type: object
      description: Button row defined on a reusable template.
      properties:
        id:
          type: string
          example: 'btn_01h2xcejqtf2nbrexx3vqjhp71'
        template:
          type: string
          example: 'btpl_01h2xcejqtf2nbrexx3vqjhp70'
        position:
          type: integer
          minimum: 1
          example: 2
        label:
          type: string
          example: Alice
        type:
          $ref: '#/components/schemas/ButtonType'
        target:
          $ref: '#/components/schemas/ButtonTarget'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
      required:
        - id
        - template
        - position
        - label
        - type
        - target
        - created_at

    DeviceButtonOverride:
      type: object
      description: Per-device button override or suppression tombstone.
      properties:
        id:
          type: string
          example: 'devbtn_01h2xcejqtf2nbrexx3vqjhp72'
        device:
          type: string
          example: 'dev_01h2xcejqtf2nbrexx3vqjhp50'
        position:
          type: integer
          minimum: 1
        suppressed:
          type: boolean
          description: When true, hides any template button at this position.
        label:
          type: [string, 'null']
        type:
          oneOf:
            - $ref: '#/components/schemas/ButtonType'
            - type: 'null'
        target:
          $ref: '#/components/schemas/ButtonTarget'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
      required:
        - id
        - device
        - position
        - suppressed
        - created_at

    MaterializedButton:
      type: object
      description: Effective button after template rows and per-device overrides are merged.
      properties:
        position:
          type: integer
          minimum: 1
        label:
          type: string
        type:
          $ref: '#/components/schemas/ButtonType'
        target:
          $ref: '#/components/schemas/ButtonTarget'
        source:
          type: string
          description: >-
            Where the button came from. `model_default` is a button the device
            model gets for free because its hardware has no other way to reach
            the function; it is stored nowhere, so it carries neither
            `template_button` nor `override`, but a device override at the same
            position shadows or suppresses it like any other row.
          enum: [template, override, template_overridden, model_default]
        template_button:
          type: [string, 'null']
        override:
          type: [string, 'null']
        compatibility:
          $ref: '#/components/schemas/ButtonCompatibilityVerdict'
      required:
        - position
        - label
        - type
        - target
        - source
        - compatibility

    CreateButtonTemplateRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          example: Front Desk
        description:
          type: string
          example: Shared front desk programmable keys
      required:
        - name

    UpdateButtonTemplateRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
        description:
          type: [string, 'null']
          description: Send null to clear the description.

    CreateTemplateButtonRequest:
      type: object
      properties:
        position:
          type: integer
          minimum: 1
          maximum: 1000
        label:
          type: string
          minLength: 1
        type:
          $ref: '#/components/schemas/ButtonType'
        target:
          $ref: '#/components/schemas/ButtonTarget'
      required:
        - position
        - label
        - type
        - target

    UpdateTemplateButtonRequest:
      type: object
      properties:
        position:
          type: integer
          minimum: 1
          maximum: 1000
          description: New position for the button.
      required:
        - position

    CreateDeviceButtonOverrideRequest:
      type: object
      description: |
        Create a device-specific button override. For suppression rows, send
        `suppressed: true` and omit `label`, `type`, and `target`.
      properties:
        position:
          type: integer
          minimum: 1
          maximum: 1000
        suppressed:
          type: boolean
          default: false
        label:
          type: string
        type:
          $ref: '#/components/schemas/ButtonType'
        target:
          $ref: '#/components/schemas/ButtonTarget'
      required:
        - position
        - suppressed

    # Device Settings Schemas

    DeviceSettings:
      type: [object, 'null']
      description: |
        Device settings with vendor-agnostic abstractions. All fields are optional —
        omitted fields inherit from the parent configuration layer
        (Global defaults → Platform → Account → Device).

        This schema evolves as new hardware capabilities are supported.
      properties:
        abstractions:
          $ref: '#/components/schemas/AbstractSettings'

    AbstractSettings:
      type: object
      description: Vendor-agnostic device configuration settings
      properties:
        audio:
          $ref: '#/components/schemas/AudioSettings'
        display:
          $ref: '#/components/schemas/DisplaySettings'
        regional:
          $ref: '#/components/schemas/RegionalSettings'
        network:
          $ref: '#/components/schemas/NetworkSettings'
        features:
          $ref: '#/components/schemas/FeatureSettings'
        provisioning:
          $ref: '#/components/schemas/ProvisioningSettings'

    AudioSettings:
      type: object
      description: Audio codec and processing settings
      properties:
        codecs:
          type: array
          description: Ordered list of preferred audio codecs
          items:
            type: string
            enum: ['PCMU', 'PCMA']
          example: ['PCMU', 'PCMA']
        vad_enabled:
          type: boolean
          description: Voice Activity Detection — suppresses silence packets to save bandwidth
        echo_cancellation:
          type: boolean
          description: Acoustic echo cancellation
        jitter_buffer:
          $ref: '#/components/schemas/JitterBuffer'

    JitterBuffer:
      type: object
      description: Jitter buffer configuration for smoothing network packet timing variations
      properties:
        mode:
          type: string
          enum: [adaptive, fixed]
          description: Buffer mode — adaptive adjusts to network conditions
        min_ms:
          type: integer
          description: Minimum buffer size in milliseconds
          example: 40
        max_ms:
          type: integer
          description: Maximum buffer size in milliseconds
          example: 200

    DisplaySettings:
      type: object
      description: Screen display preferences
      properties:
        time_format:
          type: string
          enum: ['12h', '24h']
          description: Clock display format
        date_format:
          type: string
          enum: ['M/D/Y', 'D/M/Y', 'Y-M-D']
          description: Date display format
        backlight_timeout:
          type: integer
          description: Backlight auto-off delay in seconds
          example: 30
        backlight_level:
          type: string
          enum: [low, medium, high]
          description: Screen backlight brightness

    RegionalSettings:
      type: object
      description: Localization and regional preferences
      properties:
        timezone:
          type: string
          description: IANA timezone identifier
          example: 'America/New_York'
        language:
          type: string
          description: BCP 47 language code
          example: 'en-US'
        tone_scheme:
          type: string
          description: ISO country code for dial/ring tone patterns
          example: 'us'

    NetworkSettings:
      type: object
      description: Network and QoS configuration
      properties:
        vlan_id:
          type: integer
          description: VLAN tag for voice traffic
        qos_dscp_sip:
          type: integer
          description: DSCP value for SIP signaling packets
          example: 26
        qos_dscp_rtp:
          type: integer
          description: DSCP value for RTP media packets
          example: 46
        rtcp_enabled:
          type: boolean
          description: Enable RTCP for call quality reporting
        sip_transport:
          type: string
          enum: [udp, tcp]
          description: >-
            SIP signaling transport the phone uses to reach the registrar.
            When omitted, the platform default applies. Settable at any
            configuration layer (platform, account, or device).

    FeatureSettings:
      type: object
      description: Phone feature toggles
      properties:
        dnd_enabled:
          type: boolean
          description: Do Not Disturb
        call_waiting_enabled:
          type: boolean
          description: Call waiting notification
        call_forward_enabled:
          type: boolean
          description: Call forwarding controls
        auto_answer_enabled:
          type: boolean
          description: Automatically answer incoming calls (e.g., for intercom)
        srtp_enabled:
          type: boolean
          description: Secure RTP for encrypted media

    ProvisioningSettings:
      type: object
      description: Automatic resync and firmware update settings
      properties:
        resync_time:
          type: string
          description: Time of day to resync configuration (HH:MM in 24-hour format)
          example: '02:00'
        resync_mode:
          type: string
          enum: [config_and_firmware, configuration, firmware]
          description: What to resync — configuration, firmware, or both
        bootup_check_enabled:
          type: boolean
          description: Check for configuration updates on device boot

    # Provisioning Event Schema

    ProvisioningEvent:
      type: object
      description: An audit log entry for a device provisioning request
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Provisioning event identifier
        device:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: The device that triggered this event
        event_type:
          type: string
          enum:
            - config_fetch_success
            - config_fetch_failure
            - registration
            - error
            - mac_detected
            - vendor_detected
          description: |
            Type of provisioning event:
            - `config_fetch_success` — device successfully fetched its configuration
            - `config_fetch_failure` — device failed to fetch its configuration
            - `registration` — device registered with the SIP server
            - `error` — a provisioning error occurred
            - `mac_detected` — MAC address detected from provisioning request
            - `vendor_detected` — vendor identified from MAC or User-Agent
        source_ip:
          type: [string, 'null']
          description: IP address of the device making the request
          example: '192.168.1.100'
        user_agent:
          type: string
          description: HTTP User-Agent header from the device
          example: 'snom D785/10.1.91'
        details:
          type: object
          description: Additional event metadata (error messages, vendor-specific info)
          additionalProperties: true
        created_at:
          $ref: '#/components/schemas/CreatedAt'
      required:
        - id
        - device
        - event_type
        - created_at

    # Device Schemas

    Device:
      type: object
      description: |
        A provisioned device (desk phone, DECT base station, or DECT handset).
        The `type` field indicates the device kind, and type-specific fields are only present
        for the corresponding type.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Device identifier
        type:
          type: string
          enum: [deskphone, dect_base, dect_handset]
          description: The kind of device
        mac_address:
          type: string
          description: Hardware MAC address
          example: '00:04:13:aa:bb:cc'
        vendor:
          type: string
          description: Device vendor (auto-detected from MAC address)
          example: 'snom'
        model:
          type: [string, 'null']
          description: Device model
          example: 'D785'
        name:
          type: [string, 'null']
          description: |
            Human-friendly label for the device. Set by admins on deskphones,
            DECT bases, and DECT handsets; `null` when unassigned. Handset
            responses also expose this value as `display_name` for backwards
            compatibility with older clients.
        location:
          type: [string, 'null']
          description: |
            Physical E911 location for this device. Set on deskphones and
            DECT bases; `null` when unassigned. DECT handsets have no
            `location` of their own — they inherit from the paired base
            and the handset response carries `null` here. See the E911
            guide for the dispatch resolution rules.
        status:
          type: string
          enum: [pending-sync, provisioned]
          description: Current provisioning status
        overrides:
          allOf:
            - $ref: '#/components/schemas/DeviceSettings'
          description: Device-specific settings overrides
        current_ip_address:
          type: [string, 'null']
          description: Last known IP address of the device
          example: '192.168.1.100'
        last_provisioned_at:
          type: [string, 'null']
          format: date-time
          description: When the device last fetched its configuration
        registration_status:
          type: string
          enum: [registered, not_registered]
          description: >-
            Whether the device is currently reachable (online). Derived live
            from the device's assigned lines; distinct from `status`, which only
            reflects whether the device has fetched its configuration. A device
            with no assigned lines reports `not_registered`.
        last_registered_at:
          type: [string, 'null']
          format: date-time
          description: >-
            When the device's reachability was last confirmed. Null when the
            device is not currently reachable.
        last_call_at:
          type: [string, 'null']
          format: date-time
          description: >-
            When the device last placed or received a call (the latest call
            attempt involving the device, regardless of outcome). Null until the
            device's first call. Its presence indicates the device has carried a
            call; its recency indicates whether it is still in use.
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
        primary_line:
          type: [string, 'null']
          description: Primary SIP line (deskphone only)
        lines:
          type: array
          description: SIP line assignments (deskphone only)
          items:
            $ref: '#/components/schemas/DeviceLine'
        multicell_role:
          type: [string, 'null']
          enum: [single, data_master, secondary, null]
          description: Role in a multicell deployment (DECT base only)
        max_handsets:
          type: [integer, 'null']
          description: Maximum number of handsets supported (DECT base only)
          example: 30
        firmware_version:
          type: [string, 'null']
          description: Current firmware version (DECT bases and handsets)
        handsets:
          type: array
          description: Paired handsets (DECT base only)
          items:
            $ref: '#/components/schemas/DECTHandset'
        base:
          type: [string, 'null']
          description: Parent DECT base station (DECT handset only)
        ipei:
          type: [string, 'null']
          description: DECT handset IPEI (DECT handset only)
        display_name:
          type: [string, 'null']
          description: Optional display name (DECT handset only)
        slot_number:
          type: [integer, 'null']
          description: Slot number on the base station (DECT handset only)
        registered_at:
          type: [string, 'null']
          format: date-time
          description: When the handset registered with the base (DECT handset only)
        extensions:
          type: array
          description: SIP line assignments on this handset (DECT handset only)
          items:
            $ref: '#/components/schemas/HandsetLine'
        assignments:
          type: array
          description: User assignments. Only included when `expand[]=users` is requested.
          items:
            $ref: '#/components/schemas/DeviceUserAssignment'
        compatibility:
          allOf:
            - $ref: '#/components/schemas/ButtonCompatibilitySummary'
          description: Programmable-key compatibility for the current effective button set.
        button_template_id:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - type: 'null'
          description: |
            ID of the reusable programmable-key template bound to this device.
            Null when no template is bound.
        button_template:
          oneOf:
            - $ref: '#/components/schemas/ButtonTemplate'
            - type: 'null'
          description: |
            The full programmable-key template object. Included only when
            `expand[]=button_template` is requested (the id is always available
            as `button_template_id`).
      required:
        - id
        - type
        - mac_address
        - vendor
        - status
        - registration_status
        - last_registered_at
        - last_call_at
        - created_at
        - updated_at

    UpdateDeviceRequest:
      type: object
      description: Request body for updating a device
      properties:
        model:
          type: string
          description: Device model
        name:
          type: [string, 'null']
          description: |
            Human-friendly label for the device (deskphone, DECT base, or
            DECT handset). Send `null` to clear.
        status:
          type: string
          enum: [pending-sync, provisioned]
          description: Provisioning status for deskphones and DECT bases
        overrides:
          $ref: '#/components/schemas/DeviceSettings'
        ipei:
          type: string
          description: DECT handset IPEI
        display_name:
          type: string
          deprecated: true
          description: |
            Deprecated alias of `name` for DECT handset updates. Send `name`
            instead. If both are present, `name` wins.
        location:
          type: [string, 'null']
          description: |
            Physical E911 location (deskphone or DECT base only). Tri-state:
            omit for no change, send a `loc_…` ID to set, or send explicit
            JSON `null` to clear the device's dispatch location.
        base:
          type: [string, 'null']
          description: |
            Paired DECT base (DECT handset only). Tri-state: omit for no
            change, send a `dectb_…` ID to re-pair to a different base on
            the same account, or send explicit JSON `null` to unpair the
            handset (it remains in inventory and can be paired later).
        button_template:
          type: [string, 'null']
          description: |
            Reusable programmable-key template. Tri-state: omit for no
            change, send a button template ID to bind, or send explicit
            JSON `null` to unbind.

    CreateDeviceRequest:
      type: object
      description: Request body for creating a device
      properties:
        type:
          type: string
          enum: [deskphone, dect_base, dect_handset]
          description: |
            The kind of device to create. Optional for MAC-addressable devices:
            when omitted, the type is detected from `mac_address` via the vendor
            product catalog. If the catalog cannot classify the MAC, the request
            is rejected and you must supply `type`. When you do supply `type` and
            it contradicts a positive catalog match, the request is rejected
            rather than silently corrected. `dect_handset` is IPEI-identified and
            must always be supplied explicitly.
        mac_address:
          type: string
          description: Hardware MAC address (required for deskphones and DECT bases)
          example: '00:04:13:aa:bb:cc'
        model:
          type: string
          description: Device model (optional)
          example: 'D785'
        name:
          type: string
          description: Human-friendly label for the device (deskphone, DECT base, or DECT handset).
        overrides:
          $ref: '#/components/schemas/DeviceSettings'
        multicell_role:
          type: string
          enum: [single, data_master, secondary]
          description: Multicell deployment role (DECT base only, defaults to single)
        location:
          type: string
          description: |
            Physical E911 location to assign at creation (deskphone or DECT
            base only). Can be set later via update.
          example: 'loc_01h2xcejqtf2nbrexx3vqjhp40'
        base:
          type: string
          description: |
            Parent DECT base identifier (DECT handset only). Optional —
            omit to stock the handset as unpaired; pair it later by
            updating the handset with a `base`.
          example: 'dectb_01h2xcejqtf2nbrexx3vqjhp50'
        ipei:
          type: string
          description: DECT handset IPEI (required for DECT handsets)
          example: '123456789012345'
        display_name:
          type: string
          deprecated: true
          description: |
            Deprecated alias of `name` for DECT handset creates. Send `name`
            instead. If both are present, `name` wins.

    CreateDeviceResponse:
      type: object
      description: Minimal response returned after creating a device
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Identifier of the created device
        type:
          type: string
          enum: [deskphone, dect_base, dect_handset]
          description: The kind of device that was created
      required:
        - id
        - type

    # Deskphone Schemas

    ProvisionedDevice:
      type: object
      description: A provisioned deskphone.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Device identifier
        mac_address:
          type: string
          description: Hardware MAC address
          example: '00:04:13:aa:bb:cc'
        vendor:
          type: string
          description: Device vendor (auto-detected from MAC address)
          example: 'snom'
        model:
          type: [string, 'null']
          description: Device model
          example: 'D785'
        name:
          type: [string, 'null']
          description: Human-friendly label for the device. `null` when unassigned.
        location:
          type: [string, 'null']
          description: |
            Physical E911 location for this deskphone. `null` when unassigned.
            See the E911 guide for the dispatch resolution rules.
        button_template:
          type: [string, 'null']
          description: Reusable programmable-key template bound to this deskphone.
        status:
          type: string
          enum: [pending-sync, provisioned]
          description: Current provisioning status
        overrides:
          allOf:
            - $ref: '#/components/schemas/DeviceSettings'
          description: Device-specific settings overrides
        current_ip_address:
          type: [string, 'null']
          description: Last known IP address of the device
          example: '192.168.1.100'
        last_provisioned_at:
          type: [string, 'null']
          format: date-time
          description: When the device last fetched its configuration
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
        primary_line:
          type: [string, 'null']
          description: Primary SIP line
        lines:
          type: array
          description: SIP line assignments
          items:
            $ref: '#/components/schemas/DeviceLine'
      required:
        - id
        - mac_address
        - vendor
        - status
        - created_at
        - updated_at

    CreateDeskphoneRequest:
      type: object
      description: Request body for creating a deskphone
      properties:
        mac_address:
          type: string
          description: Hardware MAC address (vendor is auto-detected)
          example: '00:04:13:aa:bb:cc'
        model:
          type: string
          description: Device model (optional)
          example: 'D785'
        overrides:
          $ref: '#/components/schemas/DeviceSettings'
      required:
        - mac_address

    UpdateDeskphoneRequest:
      type: object
      description: Request body for updating a deskphone
      properties:
        model:
          type: string
          description: Device model
        status:
          type: string
          enum: [pending-sync, provisioned]
          description: Device status
        overrides:
          $ref: '#/components/schemas/DeviceSettings'

    # DECT Handset Schemas

    DECTHandset:
      type: object
      description: A DECT handset paired with a base station
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Handset identifier
        base:
          type: [string, 'null']
          description: |
            Parent DECT base station. `null` when the handset is stocked
            but not yet paired with a base — pair later by updating this
            field with a base ID.
        ipei:
          type: string
          description: International Portable Equipment Identity
          example: '0328A0000F'
        status:
          type: string
          enum: [unpaired, pending-sync, registered, provisioned]
          description: Current handset status
        display_name:
          type: [string, 'null']
          description: Optional display name
        slot_number:
          type: integer
          description: Slot number on the base station
          example: 1
        model:
          type: [string, 'null']
          description: Handset model
        firmware_version:
          type: [string, 'null']
          description: Current firmware version
        registered_at:
          type: [string, 'null']
          format: date-time
          description: When the handset was registered with the base
        extensions:
          type: array
          description: SIP line assignments on this handset
          items:
            $ref: '#/components/schemas/HandsetLine'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - ipei
        - status
        - slot_number
        - created_at
        - updated_at

    CreateHandsetRequest:
      type: object
      description: Request body for creating a DECT handset
      properties:
        ipei:
          type: string
          description: International Portable Equipment Identity
          example: '0328A0000F'
        display_name:
          type: string
          description: Optional display name
        model:
          type: string
          description: Handset model
      required:
        - ipei

    UpdateHandsetRequest:
      type: object
      description: Request body for updating a DECT handset
      properties:
        ipei:
          type: string
          description: International Portable Equipment Identity
        display_name:
          type: string
          description: Display name
        model:
          type: string
          description: |
            Handset model (e.g. `M55`, `M58`). Handsets never provision — the
            base fetches config and pushes it over DECT — so the model can't be
            auto-detected and must be set here. It gates programmable-button
            (template) support.

    HandsetLine:
      type: object
      description: A SIP line assignment on a DECT handset
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Handset line identifier
        handset:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Parent handset
        display_name:
          type: [string, 'null']
          description: Optional display name override
        endpoint_id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: ID of the SIP endpoint assigned to this line.
        endpoint:
          oneOf:
            - type: object
              properties:
                id:
                  allOf:
                    - $ref: '#/components/schemas/EntityId'
                    - description: Endpoint identifier
                user:
                  allOf:
                    - $ref: '#/components/schemas/EntityId'
                    - description: Owning user
                name:
                  type: [string, 'null']
                  description: Endpoint display name
                status:
                  type: string
                  enum: [online, offline]
                  description: Registration status
                created_at:
                  $ref: '#/components/schemas/CreatedAt'
                updated_at:
                  $ref: '#/components/schemas/UpdatedAt'
            - type: 'null'
          description: |
            The full SIP endpoint object assigned to this line. Included on list
            responses; the id is always available as `endpoint_id`.
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - handset
        - endpoint_id
        - created_at
        - updated_at

    CreateHandsetLineRequest:
      type: object
      description: Request body for creating a handset line
      properties:
        endpoint:
          type: string
          description: ID of the SIP endpoint to assign
        display_name:
          type: string
          description: Optional display name override
      required:
        - endpoint

    # DECT Base Schemas

    DECTBase:
      type: object
      description: A DECT base station with paired wireless handsets
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: DECT base station identifier
        mac_address:
          type: string
          description: Hardware MAC address
          example: '00:04:13:bb:cc:dd'
        vendor:
          type: string
          description: Device vendor (auto-detected from MAC address)
          example: 'snom'
        model:
          type: [string, 'null']
          description: Base station model
          example: 'M500'
        name:
          type: [string, 'null']
          description: Human-friendly label for the base. `null` when unassigned.
        status:
          type: string
          enum: [pending-sync, provisioned]
          description: Current provisioning status
        multicell_role:
          type: string
          enum: [single, data_master, secondary]
          description: |
            Role in a multicell deployment:
            - `single` — standalone base station (default)
            - `data_master` — primary base that distributes configuration
            - `secondary` — subordinate base that receives configuration from the data master
        max_handsets:
          type: integer
          description: Maximum number of handsets this base supports
          example: 30
        firmware_version:
          type: [string, 'null']
          description: Current firmware version
        overrides:
          allOf:
            - $ref: '#/components/schemas/DeviceSettings'
          description: Device-specific settings overrides
        current_ip_address:
          type: [string, 'null']
          description: Last known IP address of the base station
          example: '192.168.1.101'
        last_provisioned_at:
          type: [string, 'null']
          format: date-time
          description: When the base last fetched its configuration
        location:
          type: [string, 'null']
          description: |
            Physical location used as the emergency-services dispatch address
            when any handset on this base places a call. All handsets paired
            with this base share the same location.
        handsets:
          type: array
          description: Paired wireless handsets
          items:
            $ref: '#/components/schemas/DECTHandset'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - mac_address
        - vendor
        - status
        - multicell_role
        - max_handsets
        - created_at
        - updated_at

    CreateDECTBaseRequest:
      type: object
      description: |
        Request body for creating a DECT base station via the legacy
        `/v1/dect-bases` path. To set or clear the base's E911 dispatch
        location, use the unified `/v1/devices` endpoint instead — it
        supports tri-state `location` (omit / `loc_…` / `null`).
      properties:
        mac_address:
          type: string
          description: Hardware MAC address (vendor is auto-detected)
          example: '00:04:13:bb:cc:dd'
        model:
          type: string
          description: Base station model (optional, can be auto-detected)
          example: 'M500'
        multicell_role:
          type: string
          enum: [single, data_master, secondary]
          description: Role in multicell deployment (defaults to single)
        overrides:
          $ref: '#/components/schemas/DeviceSettings'
      required:
        - mac_address

    UpdateDECTBaseRequest:
      type: object
      description: |
        Request body for updating a DECT base station via the legacy
        `/v1/dect-bases/{id}` path. See CreateDECTBaseRequest for the
        rationale on the missing `location` field.
      properties:
        model:
          type: string
          description: Base station model
        status:
          type: string
          enum: [pending-sync, provisioned]
          description: Device status
        multicell_role:
          type: string
          enum: [single, data_master, secondary]
          description: Role in multicell deployment
        overrides:
          $ref: '#/components/schemas/DeviceSettings'

    CreateCallRequest:
      type: object
      description: Request to initiate an outbound call
      properties:
        user:
          type: string
          description: The user whose endpoints will ring first. When the user answers, the destination is dialed.
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        dial_string:
          type: string
          description: |
            The destination to dial after the user answers.
            Can be a phone number (E.164 or local format), an extension, or an emergency number.
          example: '+15551234567'
      required:
        - user
        - dial_string

    CreateTestCallRequest:
      type: object
      description: |
        Request to trigger a simulated call in test mode. Available only with a
        test key (`sk_test_...`); a live key receives `400`. Used for inbound
        (screen pop) and custom-scenario simulations that a dialed number
        cannot select. See the "Testing your integration" guide.
      properties:
        user:
          type: string
          description: |
            The user to terminate the simulated call at, bypassing the account's
            inbound routing. Currently required (the endpoint returns `400`
            without it) because routing simulation is not yet supported; once it
            is, this becomes an optional shortcut for forcing a target when
            routing is not what you are testing.
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        direction:
          type: string
          enum: [inbound, outbound]
          default: inbound
          description: '`inbound` simulates a call arriving at the account; `outbound` simulates a device-originated call.'
        from_number:
          type: string
          description: Caller number for inbound calls; set it to a known contact's number to exercise caller-ID matching. Must be E.164. Ignored for outbound.
          example: '+15551234567'
        from_name:
          type: string
          description: Optional caller display name.
        to_number:
          type: string
          description: Dialed number for outbound; its magic-number value selects the scenario when no explicit `scenario` is given.
        scenario:
          $ref: '#/components/schemas/TestScenarioOverride'

    TestScenarioOverride:
      type: object
      description: Explicitly scripts a simulated call's outcome and timing, complementing the magic-number convention.
      properties:
        outcome:
          type: string
          enum: [answered, no-answer, busy, voicemail]
          description: Terminal disposition. An unrecognized value returns 400.
        ring_seconds:
          type: integer
          description: Seconds the destination rings. Clamped to 0-120.
        talk_seconds:
          type: integer
          description: Answered conversation length in seconds. Clamped to 0-120.
        recording:
          type: boolean
          description: Whether an answered call produces a recording.
        voicemail_seconds:
          type: integer
          description: Deposited voicemail length in seconds. Clamped to 0-120.

    TestCallResponse:
      type: object
      description: The started simulated call. Returned so a test harness can assert on the call id (poll the CDR, correlate webhooks).
      properties:
        id:
          type: string
          description: Identifier of the started call.
          example: 'call_01h2xcejqtf2nbrexx3vqjhp45'
        scenario:
          type: string
          description: Name of the scenario that will play out (e.g. answered, no-answer, custom).
          example: 'answered'
      required:
        - id
        - scenario

    TestEventResponse:
      type: object
      description: Echoes the emitted event type.
      properties:
        event:
          type: string
          example: 'queue.call.answered'
      required:
        - event

    # Request Bodies

    ComponentConfig:
      type: object
      description: Configuration for a single component
      properties:
        enabled:
          type: boolean
          description: Whether this component is enabled for the session
      required:
        - enabled

    CreateAccountSessionRequest:
      type: object
      description: Request body for creating an account session
      properties:
        account:
          type: string
          description: Account identifier
          example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
        components:
          type: object
          description: |
            Components to enable for this session. At least one component must have `enabled: true`.
            Each key is a component name and the value configures that component.
          additionalProperties:
            $ref: '#/components/schemas/ComponentConfig'
          properties:
            call_logs:
              $ref: '#/components/schemas/ComponentConfig'
            voicemails:
              $ref: '#/components/schemas/ComponentConfig'
            call_history:
              $ref: '#/components/schemas/ComponentConfig'
            phone_number_ordering:
              $ref: '#/components/schemas/ComponentConfig'
            phone_numbers:
              $ref: '#/components/schemas/ComponentConfig'
            account_onboarding:
              $ref: '#/components/schemas/ComponentConfig'
            dial_plan_viewer:
              $ref: '#/components/schemas/ComponentConfig'
          example:
            call_logs:
              enabled: true
            voicemails:
              enabled: true
      required:
        - account
        - components

    # Response Bodies

    CreateSessionResponse:
      type: object
      properties:
        account:
          type: string
          description: Account identifier for the created session
          example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
        client_secret:
          $ref: '#/components/schemas/ClientSecret'
        expires_at:
          $ref: '#/components/schemas/SessionExpiresAt'
      required:
        - account
        - client_secret
        - expires_at

    CreateUserSessionRequest:
      type: object
      description: Request body for creating a user session
      properties:
        user:
          type: string
          maxLength: 255
          description: |
            Identifier of the DialStack user this token will authenticate as.
            The user must already be provisioned via `POST /v1/users` and belong
            to an account owned by the calling platform.
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        ttl_seconds:
          type: integer
          minimum: 0
          maximum: 604800
          description: |
            Requested token lifetime in seconds. Defaults to 86400 (24 hours)
            when omitted or zero. Values above 604800 (7 days) are rejected.
          example: 86400
      required:
        - user

    CreateUserSessionResponse:
      type: object
      properties:
        user:
          type: string
          description: Identifier of the user the session token authenticates as
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        account:
          type: string
          description: Identifier of the account that owns the user
          example: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
        client_secret:
          $ref: '#/components/schemas/ClientSecret'
        expires_at:
          $ref: '#/components/schemas/SessionExpiresAt'
      required:
        - user
        - account
        - client_secret
        - expires_at

    RevokeUserSessionsResponse:
      type: object
      properties:
        user:
          type: string
          description: Identifier of the user whose sessions were revoked
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        sessions_revoked_at:
          type: string
          format: date-time
          description: |
            The revocation cutoff. Every user-session token minted before this
            instant is invalid; tokens minted afterwards work normally.
          example: '2026-06-07T12:00:00Z'
      required:
        - user
        - sessions_revoked_at

    # Extension Schemas

    Extension:
      type: object
      description: |
        An extension maps a short dial code (e.g., "105") to a routing target.
        Extensions provide a simple way for internal users to reach each other or
        for callers to be routed through dial plans.
      properties:
        number:
          type: string
          minLength: 1
          maxLength: 32
          description: The extension number (dial code)
          example: '105'
        target:
          type: string
          maxLength: 255
          description: ID of the routing target
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
        status:
          type: string
          enum: [active, inactive]
          description: Extension status
          example: 'active'
        created_at:
          type: string
          format: date-time
          description: When the extension was created
          example: '2025-10-18T10:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: When the extension was last modified
          example: '2025-10-18T10:00:00Z'
      required:
        - number
        - target
        - status
        - created_at
        - updated_at

    CreateExtensionRequest:
      type: object
      description: Request body for creating a new extension
      properties:
        number:
          type: string
          minLength: 1
          maxLength: 32
          description: The extension number (dial code)
          example: '105'
        target:
          type: string
          maxLength: 255
          description: ID of the routing target
          example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
      required:
        - number
        - target

    UpdateExtensionRequest:
      type: object
      description: Request body for updating an extension (all fields optional)
      properties:
        target:
          type: string
          maxLength: 255
          description: ID of the new routing target
          example: 'user_01h2xcejqtf2nbrexx3vqjhp43'
        number:
          type: string
          pattern: '^[0-9]{1,20}$'
          description: >
            New extension number. When provided, the extension is renamed to
            this number. Must be unique within the account (a collision returns
            409).
          example: '1200'

    CreateNumberOrderRequest:
      type: object
      description: Request body for ordering phone numbers
      properties:
        phone_numbers:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 100
          description: Phone numbers to order (E.164 format)
          example: ['+19195551234', '+19195555678']
        partial_allowed:
          type: boolean
          default: false
          description: >
            When true, the order may partially complete — available numbers are
            provisioned and unavailable ones are marked as failed. When false
            (default), the entire order fails if any number is unavailable.
      required:
        - phone_numbers

    # Location Management

    Address:
      type: object
      description: A validated, normalized physical address (embedded value object)
      properties:
        place_id:
          type: string
          description: Geocoding place identifier
        address_number:
          type: string
          description: House or building number
        street:
          type: string
          description: Street name
        unit:
          type: string
          description: Unit, suite, or apartment number
        city:
          type: string
          description: City or locality
          example: 'New York'
        state:
          type: string
          description: State or province code
          example: 'NY'
        postal_code:
          type: string
          description: Postal or ZIP code
          example: '10001'
        country:
          type: string
          description: Two-letter country code
          example: 'US'
        latitude:
          type: number
          format: double
          description: WGS 84 latitude
        longitude:
          type: number
          format: double
          description: WGS 84 longitude
        formatted_address:
          type: string
          description: Computed formatted address string
          example: '123 Main St, New York, NY 10001, US'
      required:
        - city
        - state
        - postal_code
        - country
        - formatted_address

    Location:
      type: object
      description: A business location with a validated physical address
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        name:
          type: string
          maxLength: 255
          description: Display name of the location
          example: 'Main Office'
        address:
          $ref: '#/components/schemas/Address'
        primary_did:
          type: [string, 'null']
          deprecated: true
          description: |
            Deprecated. Historical main phone number for the location. This
            field does not affect E911 provisioning or emergency dispatch.
        status:
          type: string
          enum: [active, inactive]
          deprecated: true
          description: |
            Deprecated. The operational-status field was retired; the API
            always returns `active`. Do not branch on this value.
          example: 'active'
        e911_status:
          type: string
          enum: [none, pending, binding, provisioned, failed]
          description: E911 provisioning status for this location. Managed by the provider; read-only.
          readOnly: true
          example: 'none'
        config:
          $ref: '#/components/schemas/LocationConfig'
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - name
        - address
        - status
        - e911_status
        - created_at
        - updated_at

    E911ValidationResponse:
      type: object
      description: Result of E911 address validation
      properties:
        adjusted:
          type: boolean
          description: Whether the address was adjusted during geocoding
        address:
          type: object
          description: The geocoded address used for E911 registration
          properties:
            house_number:
              type: string
              description: House or building number
            street_name:
              type: string
              description: Street name
            pre_directional:
              type: string
              description: Street name pre-directional (e.g., N, S, E, W)
            street_suffix:
              type: string
              description: Street type suffix (e.g., ST, AVE)
            post_directional:
              type: string
              description: Street name post-directional (e.g., N, S, E, W)
            address_line_2:
              type: string
              description: Secondary address line (e.g., Suite, Apt, Floor)
            city:
              type: string
              description: City name
            state_code:
              type: string
              description: Two-letter state code
            zip:
              type: string
              description: ZIP code
            plus_four:
              type: string
              description: ZIP+4 extension
            county:
              type: string
              description: County name
            country:
              type: string
              description: Country code
      required:
        - adjusted

    AddressInput:
      type: object
      description: Address fields for creating or updating a location
      properties:
        address_number:
          type: string
          maxLength: 255
          description: House or building number
          example: '1600'
        street:
          type: string
          maxLength: 255
          description: Street name
          example: 'Pennsylvania Avenue NW'
        unit:
          type: string
          maxLength: 255
          description: Unit, suite, or apartment number
          example: 'Suite 200'
        city:
          type: string
          maxLength: 255
          description: City or locality
          example: 'New York'
        state:
          type: string
          description: State or province code
          example: 'NY'
        postal_code:
          type: string
          description: Postal or ZIP code
          example: '10001'
        country:
          type: string
          enum: [US, CA, MX]
          description: Two-letter country code
          example: 'US'
      required:
        - street
        - city
        - state
        - postal_code
        - country

    CreateLocationRequest:
      type: object
      description: Request body for creating a location
      properties:
        name:
          type: string
          maxLength: 255
          description: Display name of the location
          example: 'Main Office'
        address:
          $ref: '#/components/schemas/AddressInput'
        primary_did:
          type: string
          deprecated: true
          description: |
            Deprecated. Historical main phone number for the location. This
            field does not affect E911 provisioning or emergency dispatch.
      required:
        - name
        - address

    UpdateLocationRequest:
      type: object
      description: Request body for updating a location (all fields optional)
      properties:
        name:
          type: string
          maxLength: 255
          description: Display name of the location
          example: 'Updated Office'
        address:
          $ref: '#/components/schemas/AddressInput'
        primary_did:
          type: [string, 'null']
          deprecated: true
          description: |
            Deprecated. Historical main phone number for the location. This
            field does not affect E911 provisioning or emergency dispatch.
        status:
          type: string
          enum: [active, inactive]
          deprecated: true
          description: |
            Deprecated. The operational-status field was retired; the value is
            accepted for backwards compatibility but ignored.
        config:
          $ref: '#/components/schemas/LocationConfig'

    # Port Order Schemas

    PortOrder:
      type: object
      description: |
        A port order tracks the process of transferring (porting) existing phone numbers
        from another carrier to DialStack. Port orders follow a draft-based
        workflow: create a draft, have the customer approve the order, then submit
        for processing.

        ## Status Lifecycle

        ```
        draft → approved → submitted → foc (scheduled) → complete
                                      → exception → (update + resubmit) → submitted
                                      → cancelled
        draft → cancelled
        approved → cancelled
        ```

        - `draft` — Order created, not yet approved
        - `approved` — Customer has approved the order, ready to submit
        - `submitted` — Sent to the carrier, awaiting processing
        - `exception` — Rejected by the carrier (can be corrected and resubmitted)
        - `foc` — The port is scheduled (a Firm Order Commitment has been received from the carrier confirming the completion date)
        - `complete` — Numbers successfully ported and active
        - `cancelled` — Order was cancelled
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          example: 'por_01h2xcejqtf2nbrexx3vqjhp53'
        status:
          type: string
          enum: [draft, approved, submitted, exception, foc, complete, cancelled]
          description: Current status of the port order
          example: 'draft'
        details:
          $ref: '#/components/schemas/PortOrderDetails'
        submitted_at:
          type: [string, 'null']
          format: date-time
          description: When the order was submitted for processing
          example: '2026-02-15T14:30:00Z'
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - status
        - details
        - created_at
        - updated_at

    PortOrderDetails:
      type: object
      description: Details of the port order including phone numbers, subscriber information, and document metadata
      properties:
        phone_numbers:
          type: array
          items:
            type: string
          description: Phone numbers to port (E.164 format)
          example: ['+12025551234']
        subscriber:
          $ref: '#/components/schemas/PortSubscriber'
        requested_foc_date:
          type: string
          format: date
          description: Requested date for the port to complete (YYYY-MM-DD). Must be at least 5 business days and no more than 30 calendar days from today. "FOC" stands for Firm Order Commitment.
          example: '2026-03-01'
        requested_foc_time:
          type: [string, 'null']
          description: Requested time for the port to complete, in HH:MM format (08:00-20:00 Eastern). Optional.
          example: '10:00'
        actual_foc_date:
          type: [string, 'null']
          format: date-time
          description: Actual scheduled date/time confirmed by the carrier
          example: '2026-03-01T10:00:00Z'
        losing_carrier:
          oneOf:
            - $ref: '#/components/schemas/PortCarrier'
            - type: 'null'
          description: Losing carrier information (populated after submission)
        approval:
          oneOf:
            - $ref: '#/components/schemas/PortApproval'
            - type: 'null'
          description: Customer approval details (populated when the order is approved)
        rejection:
          oneOf:
            - $ref: '#/components/schemas/PortRejection'
            - type: 'null'
          description: Rejection details (populated when status is exception)
        loa:
          oneOf:
            - $ref: '#/components/schemas/PortDocumentMeta'
            - type: 'null'
          description: Authorization document metadata (generated automatically on submission)
        csr:
          oneOf:
            - $ref: '#/components/schemas/PortDocumentMeta'
            - type: 'null'
          description: Customer Service Record document metadata
        bill_copy:
          oneOf:
            - $ref: '#/components/schemas/PortDocumentMeta'
            - type: 'null'
          description: Bill copy document metadata
      required:
        - phone_numbers

    PortSubscriber:
      type: object
      description: Subscriber and authorization information for the port request
      properties:
        btn:
          type: string
          description: Billing Telephone Number (E.164 format)
          example: '+12025551234'
        business_name:
          type: string
          description: Business or account name as it appears on the invoice from the current service provider
          example: 'Doe Enterprises'
        approver_name:
          type: string
          description: Full name of the person who will approve the port order
          example: 'John Doe'
        account_number:
          type: [string, 'null']
          description: Account number with the losing carrier. Required for some wireless ports — check the `account_number_required` field in the eligibility response.
          example: '123456789'
        pin:
          type: [string, 'null']
          description: PIN or password for the losing carrier account. Often required alongside `account_number` for wireless ports.
          example: '1234'
        address:
          $ref: '#/components/schemas/PortAddress'
      required:
        - btn
        - business_name
        - approver_name
        - address

    PortAddress:
      type: object
      description: Service address for the port request
      properties:
        house_number:
          type: string
          description: House or building number
          example: '123'
        street_name:
          type: string
          description: Street name (without house number)
          example: 'Main St'
        line2:
          type: [string, 'null']
          description: Apartment, suite, unit, etc. (optional)
          example: 'Suite 200'
        city:
          type: string
          description: City
          example: 'Anytown'
        state:
          type: string
          minLength: 2
          maxLength: 2
          description: Two-letter state code (auto-uppercased)
          example: 'VA'
        zip:
          type: string
          description: ZIP code
          example: '22030'
      required:
        - house_number
        - street_name
        - city
        - state
        - zip

    PortCarrier:
      type: object
      description: Losing carrier information
      properties:
        name:
          type: string
          description: Carrier name
          example: 'Verizon'
        spid:
          type: string
          description: Carrier SPID
          example: '1234'
        port_type:
          type: string
          description: Port type (e.g., automated, manual)
          example: 'automated'

    PortRejection:
      type: object
      description: Rejection details from the carrier
      properties:
        code:
          type: string
          description: Rejection reason code
          enum:
            - address_mismatch
            - account_number_invalid
            - name_mismatch
            - business_name_mismatch
            - btn_mismatch
            - pin_required
            - pin_invalid
            - port_date_invalid
            - mixed_carrier
            - pending_order
            - tn_not_portable
            - carrier_freeze
            - type_of_service_mismatch
            - carrier_system_error
            - unknown
          example: 'address_mismatch'
        message:
          type: string
          description: Human-readable rejection message
          example: 'Service address mismatch'

    PortApproval:
      type: object
      description: Customer approval details for a port order
      properties:
        signature:
          type: string
          description: Electronic signature (the customer's typed name)
          example: 'Jane Smith'
        ip:
          type: string
          description: IP address of the customer at the time of approval
          example: '203.0.113.42'
        timestamp:
          type: string
          format: date-time
          description: When the approval was given
          example: '2026-02-15T14:02:00Z'
      required:
        - signature
        - ip
        - timestamp

    ApprovePortOrderRequest:
      type: object
      description: Request body for approving a port order
      properties:
        signature:
          type: string
          description: Electronic signature (the customer's typed name authorizing the port)
          example: 'Jane Smith'
        ip:
          type: string
          description: IP address of the customer approving the order
          example: '203.0.113.42'
      required:
        - signature
        - ip

    PortDocumentMeta:
      type: object
      description: Metadata about a port order document
      properties:
        content_type:
          type: string
          description: MIME type of the document
          example: 'application/pdf'
        file_size:
          type: integer
          description: File size in bytes
          example: 123456
      required:
        - content_type
        - file_size

    PortOrderEvent:
      type: object
      description: An audit event recording a status change on a port order
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          example: 'poe_01h2xcejqtf2nbrexx3vqjhp54'
        port_order:
          allOf:
            - $ref: '#/components/schemas/EntityId'
          example: 'por_01h2xcejqtf2nbrexx3vqjhp53'
        old_status:
          type: [string, 'null']
          enum: [draft, approved, submitted, exception, foc, complete, cancelled, null]
          description: Previous status (null for the initial creation event)
        new_status:
          type: string
          enum: [draft, approved, submitted, exception, foc, complete, cancelled]
          description: New status after this event
        bandwidth_status:
          type: [string, 'null']
          description: Raw status string from the carrier (if applicable)
        message:
          type: [string, 'null']
          description: Human-readable event message
        reason_code:
          type: [string, 'null']
          description: Raw upstream carrier error code for debugging (if applicable). The mapped rejection reason is in the order's `details.rejection.code` field.
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
      required:
        - id
        - port_order
        - new_status
        - created_at

    CreatePortOrderRequest:
      type: object
      description: Request body for creating a draft port order
      properties:
        phone_numbers:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 100
          description: US phone numbers to port (E.164 format, no toll-free)
          example: ['+12025551234']
        subscriber:
          $ref: '#/components/schemas/PortSubscriber'
        requested_foc_date:
          type: string
          format: date
          description: Requested date for the port to complete (YYYY-MM-DD). Must be at least 5 business days and no more than 30 calendar days from today (excludes weekends and US federal holidays).
          example: '2026-03-01'
        requested_foc_time:
          type: string
          description: Requested time for the port to complete, in HH:MM format (08:00-20:00 Eastern). Optional.
          example: '10:00'
      required:
        - phone_numbers
        - subscriber
        - requested_foc_date

    UpdatePortOrderRequest:
      type: object
      description: |
        Request body for updating a port order in `draft`, `approved`, or `exception` status.
        All fields are optional — only provided fields are updated.
        Updating an approved order clears the approval and reverts the status to `draft`.
        For exception orders, setting `resubmit: true` clears the rejection and re-submits.
      properties:
        phone_numbers:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 100
          description: Updated phone numbers (E.164 format, no toll-free)
          example: ['+12025551234']
        subscriber:
          $ref: '#/components/schemas/PortSubscriber'
        requested_foc_date:
          type: string
          format: date
          description: Updated requested completion date (YYYY-MM-DD). Must be at least 5 business days and no more than 30 calendar days from today.
          example: '2026-03-15'
        requested_foc_time:
          type: string
          description: Updated requested completion time in HH:MM format (08:00-20:00 Eastern)
          example: '10:00'
        resubmit:
          type: boolean
          description: When true and the order is in `exception` status, clears the rejection and re-submits to the carrier.
          default: false

    PortInEligibilityRequest:
      type: object
      description: Request body for checking port-in eligibility
      properties:
        phone_numbers:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 100
          description: US phone numbers to check (E.164 format, no toll-free)
          example: ['+12025551234', '+14155559876']
      required:
        - phone_numbers

    PortInEligibilityResult:
      type: object
      description: Port-in eligibility check result
      required:
        - portable_numbers
        - non_portable_numbers
      properties:
        portable_numbers:
          type: array
          items:
            $ref: '#/components/schemas/PortableNumber'
          description: Numbers that can be ported
        non_portable_numbers:
          type: array
          items:
            $ref: '#/components/schemas/NonPortableNumber'
          description: Numbers that cannot be ported

    PortableNumber:
      type: object
      description: A phone number that is eligible for porting
      properties:
        phone_number:
          type: string
          description: Phone number in E.164 format
          example: '+14155559876'
        losing_carrier_name:
          type: string
          description: Name of the current carrier
          example: 'T-Mobile US-SVR-10X/2'
        losing_carrier_spid:
          type: string
          description: Carrier SPID
          example: '6529'
        is_wireless:
          type: boolean
          description: Whether this is a wireless number
          example: true
        account_number_required:
          type: boolean
          description: Whether the losing carrier requires an account number for porting. When true, include `account_number` (and typically `pin`) in the port order subscriber details.
          example: false
      required:
        - phone_number
        - is_wireless
        - account_number_required

    NonPortableNumber:
      type: object
      description: A phone number that cannot be ported
      properties:
        phone_number:
          type: string
          description: Phone number in E.164 format
          example: '+12025559999'
        rate_center:
          type: string
          description: Rate center where the number is located
          example: 'WASHINGT DC'
        city:
          type: string
          description: City
          example: 'Washington'
        state:
          type: string
          description: State
          example: 'DC'
      required:
        - phone_number

    # Error Responses

    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message
          example: 'Invalid request body'
        code:
          type: string
          description: Machine-readable error code
          example: 'validation_error'
        details:
          type: object
          additionalProperties: true
          description: Additional error context (field-specific validation errors, etc.)
      required:
        - error

    DeviceUserConflictResponse:
      type: object
      description: |
        409 response for `POST /v1/devices/{id}/users` when the assignment
        violates the at-most-one-device-per-user rule. Extends Error with
        a stable `code` and, when applicable, the ID of the device the
        target user is already assigned to. The admin UI uses
        `existing_device` to drive its reassign-confirm flow without
        string-matching on `error`.
      properties:
        error:
          type: string
          description: Human-readable error message
          example: 'user is already assigned to another device'
        code:
          type: string
          enum:
            - user_already_assigned
            - user_already_has_device
            - user_already_has_endpoint
            - handset_already_assigned
            - device_full
            - base_full
          description: Machine-readable error code
          example: 'user_already_has_device'
        existing_device:
          type: string
          description: |
            Identifier of the device the user is already assigned to.
            Present on `user_already_has_device`; omitted on other codes.
          example: 'dev_01h2xcejqtf2nbrexx3vqjhp40'
      required:
        - error
        - code

paths:
  # Account Management (Platform-level, no DialStack-Account header required)

  /v1/accounts:
    post:
      summary: Create a new account
      description: |
        Creates a new account (customer organization) within the platform.
        Accounts represent individual customer organizations (e.g., Jones Chiropractic)
        that use the vSaaS platform.

      operationId: createAccount
      tags: [Accounts]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAccountRequest'
            examples:
              basic:
                summary: Basic account creation
                value:
                  email: 'contact@example.com'
                  primary_contact_name: 'Jane Doe'
                  billing_address:
                    street: '123 Main St'
                    city: 'New York'
                    state: 'NY'
                    postal_code: '10001'
                    country: 'US'
                  pricing:
                    per_user_rate: 1999
                    per_did_rate: 299
                    per_voiceai_location_rate: 4999
              with_config:
                summary: Account with configuration
                value:
                  email: 'contact@example.com'
                  primary_contact_name: 'Jane Doe'
                  config:
                    region: 'US'
                    timezone: 'America/New_York'
                    extension_length: 4
                  billing_address:
                    street: '123 Main St'
                    city: 'New York'
                    state: 'NY'
                    postal_code: '10001'
                    country: 'US'
                  pricing:
                    per_user_rate: 1999
                    per_did_rate: 299
                    per_voiceai_location_rate: 4999
      responses:
        '201':
          description: Account created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
              examples:
                success:
                  summary: Successful account creation
                  value:
                    id: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
                    email: 'contact@example.com'
                    config: {}
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      summary: List all accounts
      operationId: listAccounts
      tags: [Accounts]
      description: |
        List all accounts for the authenticated platform. Results are returned
        in reverse chronological order (newest first).

        Every account carries its subscription-agreement (SSA/TOS) coverage as
        `tos_status` (and `tos_accepted_at`), so callers can build an
        agreement-coverage view — e.g. surfacing the live accounts still blocked
        from PSTN (`tos_status: "unsigned"`) until they accept.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of accounts
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Account'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                list:
                  summary: Account list response
                  value:
                    object: 'list'
                    url: '/v1/accounts'
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - id: 'acct_01h2xcejqtf2nbrexx3vqjhp41'
                        email: 'contact@example.com'
                        config: {}
                        created_at: '2025-10-18T10:00:00Z'
                        updated_at: '2025-10-18T10:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/accounts/{account_id}:
    parameters:
      - $ref: '#/components/parameters/AccountId'
    get:
      summary: Get account details
      operationId: getAccount
      tags: [Accounts]
      description: |
        Retrieve a specific account by ID. The response carries the account's
        subscription-agreement coverage as `tos_status` / `tos_accepted_at`; the
        full agreement document is on `GET /v1/accounts/{account_id}/tos`.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Account details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/AccountNotFound'
    post:
      summary: Update account
      description: |
        Update account details.

      operationId: updateAccount
      tags: [Accounts]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAccountRequest'
      responses:
        '200':
          description: Account updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/AccountNotFound'
        '409':
          description: |
            Returned when the supplied `main_location` or
            `default_button_template` was modified or deleted in a concurrent
            request. Retry with another active resource.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      summary: Delete account
      description: |
        Delete an account and all associated users, endpoints, and call logs.
        This operation is irreversible. Active calls will be terminated.

      operationId: deleteAccount
      tags: [Accounts]
      security:
        - BearerAuth: []
      responses:
        '204':
          $ref: '#/components/responses/AccountDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/AccountNotFound'

  /v1/accounts/{account_id}/pricing:
    parameters:
      - $ref: '#/components/parameters/AccountId'
    get:
      summary: Get agreed pricing
      description: |
        Retrieve the agreed retail pricing for an account. Rates are the
        platform's retail prices in cents, per month.

      operationId: getAccountPricing
      tags: [Accounts]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Agreed pricing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountPricing'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/AccountNotFound'
    post:
      summary: Set agreed pricing
      description: |
        Create or replace the agreed retail pricing for an account. All three
        rates are required, in cents.

      operationId: upsertAccountPricing
      tags: [Accounts]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccountPricingRequest'
      responses:
        '200':
          description: Agreed pricing set
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountPricing'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/AccountNotFound'

  /v1/accounts/{account_id}/tos:
    parameters:
      - $ref: '#/components/parameters/AccountId'
    get:
      summary: Get the subscription agreement
      description: |
        Returns the account's subscription-agreement resource: the current
        agreement document and the account's acceptance state (null until
        accepted). Pass `expand[]=pricing` to also include the account's agreed
        pricing (what the customer is accepting).

      operationId: getAccountTos
      tags: [Accounts]
      security:
        - BearerAuth: []
      parameters:
        - name: 'expand[]'
          in: query
          required: false
          description: Related resources to expand. Supports `pricing`.
          schema:
            type: array
            items:
              type: string
              enum: [pricing]
      responses:
        '200':
          description: Agreement resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tos'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/AccountNotFound'
    post:
      summary: Accept the subscription agreement
      description: |
        Record the account's acceptance of the subscription agreement and
        return the updated agreement resource. Acceptance may be recorded either
        by the account owner accepting in-portal (an account session) or by the
        platform on the owner's behalf using its secret API key — by calling
        this endpoint with your API key you assert that you presented the
        agreement to the account owner and the owner accepted. Any other
        credential is rejected (403). The submitted version must match the
        current agreement (409 otherwise). The account's pricing must be set
        first, since the agreement embeds it (422 otherwise); the accepted
        pricing is snapshotted onto the acceptance record. Re-accepting the same
        version is idempotent.

      operationId: acceptAccountTos
      tags: [Accounts]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TosAcceptanceRequest'
      responses:
        '200':
          description: Acceptance recorded (or unchanged, when the same version was already accepted)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tos'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: The presented credential is not permitted to accept the agreement (only an account session or the platform's secret API key may accept)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/AccountNotFound'
        '409':
          description: Agreement version is out of date; re-fetch and accept the current version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Account pricing must be set before the agreement can be accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # Session Management

  /v1/account_sessions:
    post:
      summary: Create an account session
      description: |
        Creates a session with account-scoped access. Sessions expire after 1 hour.
        The session token is scoped to the specified components — it can only access
        API endpoints required by those components.

        The account and components are specified in the request body.
      operationId: createAccountSession
      tags: [Sessions]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAccountSessionRequest'
      responses:
        '201':
          description: Session created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/AccountNotFound'

  /v1/user_sessions:
    post:
      summary: Create a user session
      description: |
        Mints a short-lived JWT scoped to a single DialStack user. The returned
        `client_secret` is used by the WebRTC client SDK to authenticate to the
        signalling WebSocket and user-scoped REST routes such as
        `/v1/me/emergency-addresses`. (The broader `/v1/me/*` surface — profile,
        call history, presence — is documented ahead of release and not yet
        available.)

        Sessions default to a 24-hour lifetime and may not exceed 7 days. The
        user must already exist under an account owned by the calling platform.
        Outstanding sessions can be invalidated at any time with
        `POST /v1/users/{user_id}/revoke_sessions`.
      operationId: createUserSession
      tags: [Sessions]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserSessionRequest'
      responses:
        '201':
          description: User session created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateUserSessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UserNotFound'

  /v1/users/{user_id}/revoke_sessions:
    post:
      summary: Revoke a user's sessions
      description: |
        Invalidates every outstanding user-session token for the user, all at
        once. Tokens minted before this call stop working immediately on REST
        routes and at WebRTC connection time; active WebRTC connections are
        torn down at their next call event (placing or receiving a call).

        Use this as the server-side kill switch when a user must be cut off
        before their tokens expire (offboarding, compromised device, abuse).
        New sessions can be minted for the user immediately afterwards.
      operationId: revokeUserSessions
      tags: [Sessions]
      security:
        - BearerAuth: []
      parameters:
        - name: user_id
          in: path
          required: true
          description: Identifier of the user whose sessions to revoke
          schema:
            type: string
      responses:
        '200':
          description: Sessions revoked successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RevokeUserSessionsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UserNotFound'

  # Audit Logs

  /v1/audit-logs:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    get:
      summary: List audit logs
      description: |
        List audit log entries for an account with optional filtering.
        Results are returned in reverse chronological order (newest first).
      operationId: listAuditLogs
      tags: [Audit Logs]
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: event
          in: query
          description: Filter by event type (e.g., `user.create`, `auth.denied`). Supports comma-separated values.
          schema:
            type: string
        - name: resource
          in: query
          description: Filter by resource ID (exact match)
          schema:
            type: string
        - name: actor
          in: query
          description: Filter by actor ID (exact match)
          schema:
            type: string
        - name: outcome
          in: query
          description: Filter by outcome
          schema:
            type: string
            enum: [success, failure, denied]
        - name: ip_address
          in: query
          description: Filter by IP address
          schema:
            type: string
        - name: created_after
          in: query
          description: Filter events created at or after this time (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: created_before
          in: query
          description: Filter events created at or before this time (ISO 8601)
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: A paginated list of audit log entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AuditLogEntry'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'

  # User Management (Requires account context)

  /v1/users:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Create a new user
      description: |
        Create a new user within the account. Users represent people who have phone service.
        Each user can have multiple endpoints (devices).

      operationId: createUser
      tags: [Users]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
            examples:
              basic:
                summary: Basic user
                value:
                  name: 'Dr. Alice Smith'
                  email: 'alice@spineline.dev'
      responses:
        '201':
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                success:
                  summary: Successful user creation
                  value:
                    id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                    name: 'Dr. Alice Smith'
                    email: 'alice@spineline.dev'
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    get:
      summary: List all users
      description: |
        List all users for the account.
        Results are returned in reverse chronological order (newest first).

        **Expandable fields:** `extensions` — includes the list of extensions assigned to each user.

      operationId: listUsers
      tags: [Users]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: search
          in: query
          description: Filter users by name or email (case-insensitive partial match)
          schema:
            type: string
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                list:
                  summary: User list response
                  value:
                    object: 'list'
                    url: '/v1/users'
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                        name: 'Dr. Alice Smith'
                        email: 'alice@spineline.dev'
                        created_at: '2025-10-18T10:00:00Z'
                        updated_at: '2025-10-18T10:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/users/{user_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/UserId'
    get:
      summary: Get user details
      description: |
        Retrieve a specific user by ID.

        **Expandable fields:** `extensions` — includes the list of extensions assigned to this user.

      operationId: getUser
      parameters:
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      tags: [Users]
      responses:
        '200':
          description: User details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                success:
                  summary: User details
                  value:
                    id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                    name: 'Dr. Alice Smith'
                    email: 'alice@spineline.dev'
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UserNotFound'
    post:
      summary: Update user
      description: |
        Update user details (name, email, etc.)

      operationId: updateUser
      tags: [Users]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
      responses:
        '200':
          description: User updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                success:
                  summary: User updated successfully
                  value:
                    id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                    name: 'Dr. Alice Smith'
                    email: 'alice@spineline.dev'
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UserNotFound'
    delete:
      summary: Delete user
      description: |
        Delete a user and all associated endpoints. This operation is irreversible.
        Active calls will be terminated.

      operationId: deleteUser
      tags: [Users]
      responses:
        '204':
          $ref: '#/components/responses/UserDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UserNotFound'

  # Voicemail Management

  /v1/voicemails:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    get:
      summary: List voicemails
      description: |
        List voicemails visible to the authenticated account, across both
        per-user mailboxes and shared voicemail boxes.

        Results are returned in reverse chronological order (newest first).
        Use `owner` (a user ID or shared voicemail box ID) to scope results
        to a single owner.

      operationId: listVoicemails
      tags: [Voicemails]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: owner
          in: query
          description: Filter to voicemails belonging to a specific owner (a user ID or a shared voicemail box ID).
          schema:
            type: string
        - name: is_read
          in: query
          description: Filter by read status.
          schema:
            type: boolean
        - name: from_date
          in: query
          description: Return voicemails created on or after this date (ISO 8601).
          schema:
            type: string
            format: date-time
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `call`.'
          schema:
            type: array
            items:
              type: string
              enum: [call]
      responses:
        '200':
          description: List of voicemails
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Voicemail'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/voicemails/{voicemail_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/VoicemailId'
    get:
      summary: Get a voicemail
      description: |
        Retrieve a specific voicemail by ID, including the `audio_url` for
        playback. Voicemail IDs are globally unique, so no owner context is
        required in the URL.

      operationId: getVoicemail
      tags: [Voicemails]
      parameters:
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `call`.'
          schema:
            type: array
            items:
              type: string
              enum: [call]
      responses:
        '200':
          description: Voicemail details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Voicemail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/VoicemailNotFound'
    post:
      summary: Update a voicemail
      description: |
        Update voicemail status (mark as read/unread).

      operationId: updateVoicemail
      tags: [Voicemails]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateVoicemailRequest'
            examples:
              mark_read:
                summary: Mark as read
                value:
                  is_read: true
              mark_unread:
                summary: Mark as unread
                value:
                  is_read: false
      responses:
        '200':
          description: Voicemail updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Voicemail'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/VoicemailNotFound'
    delete:
      summary: Delete a voicemail
      description: |
        Permanently delete a voicemail and its audio recording.
        This operation is irreversible.

      operationId: deleteVoicemail
      tags: [Voicemails]
      responses:
        '204':
          $ref: '#/components/responses/VoicemailDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/VoicemailNotFound'

  /v1/voicemails/{voicemail_id}/transcript:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/VoicemailId'
    get:
      summary: Get a voicemail transcript
      description: |
        Retrieve the transcript for a specific voicemail.

        **Transcription Status:**
        - `pending`: The voicemail has been saved but transcription has not started
        - `processing`: Transcription is currently in progress
        - `completed`: Transcription is complete and text is available
        - `failed`: Transcription failed (text will be null)

        Voicemails are automatically transcribed after being saved.

      operationId: getVoicemailTranscript
      tags: [Voicemails]
      responses:
        '200':
          description: Voicemail transcript
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoicemailTranscript'
              examples:
                completed:
                  summary: Completed transcription
                  value:
                    voicemail: 'vm_01h2xcejqtf2nbrexx3vqjhp45'
                    status: 'completed'
                    text: 'Hi, this is John Smith calling about my appointment tomorrow...'
                pending:
                  summary: Pending transcription
                  value:
                    voicemail: 'vm_01h2xcejqtf2nbrexx3vqjhp45'
                    status: 'pending'
                    text: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/VoicemailNotFound'

  /v1/voicemail_greetings/{owner}/{type}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: owner
        in: path
        required: true
        description: ID of the mailbox owner — either a user or a shared voicemail box.
        schema:
          type: string
      - name: type
        in: path
        required: true
        description: |
          The greeting variant to operate on. Today only `unavailable` is
          supported.
        schema:
          type: string
          enum: [unavailable]

    post:
      summary: Upload a voicemail greeting
      description: |
        Uploads (or replaces) a custom greeting for a user mailbox or a
        shared voicemail box.

        Accepts a multipart form with a `file` field containing the audio.
        The audio is validated and transcoded server-side to the storage
        format (mono µ-law 8 kHz WAV).

        - Maximum file size: 5 MB.
        - Maximum duration: 90 seconds.
        - Supported input formats: WAV (PCM s16 / µ-law / A-law), MP3, AAC, Ogg Vorbis, Opus.

        Re-uploading for the same `(owner, type)` overwrites the existing
        greeting.

      operationId: uploadVoicemailGreeting
      tags: [Voicemails]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: Audio file to upload.
              required:
                - file
      responses:
        '200':
          description: Greeting uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoicemailGreetingResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Owner not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: File size exceeds the 5 MB limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      summary: Get a voicemail greeting
      description: |
        Returns the current custom greeting for a mailbox owner, including a
        short-lived signed URL for audio playback.

      operationId: getVoicemailGreeting
      tags: [Voicemails]
      responses:
        '200':
          description: Greeting details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoicemailGreetingResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No custom greeting is set for this (owner, type)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    delete:
      summary: Delete a voicemail greeting
      description: |
        Removes the custom greeting for a mailbox owner. The mailbox reverts
        to the system-default prompts.

        This operation is idempotent — returns `204 No Content` whether or
        not a greeting was previously set.

      operationId: deleteVoicemailGreeting
      tags: [Voicemails]
      responses:
        '204':
          description: Greeting removed (or was already absent)
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # Calls

  /v1/test_helpers/calls:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Trigger a simulated call (test mode only)
      description: |
        Triggers a simulated call in test mode, for exercising the event-driven
        surface (webhooks and SSE) without placing a real phone call. Primarily
        for inbound (screen pop) and custom-scenario simulations that a dialed
        number cannot select.

        Available only with a test key (`sk_test_...`); a live key receives a
        `400` naming the mistake. Asynchronous: a `200` indicates the simulation
        was started (its id + scenario are returned), not completed. See the
        "Testing your integration" guide.
      operationId: createTestCall
      tags: [Calls]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTestCallRequest'
            examples:
              inbound_screen_pop:
                summary: Inbound call for screen-pop testing
                value:
                  user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                  from_number: '+15551234567'
              custom_voicemail:
                summary: Inbound call that lands in voicemail
                value:
                  user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                  from_number: '+15551234567'
                  from_name: 'Sarah Johnson'
                  scenario:
                    outcome: voicemail
                    ring_seconds: 15
                    voicemail_seconds: 12
      responses:
        '200':
          description: Simulation started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestCallResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/test_helpers/events:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Emit a canned webhook event (test mode only)
      description: |
        Emits a single webhook event of the requested type with a
        representative sample payload, through the normal webhook delivery
        path. Complements the behavioral call simulation by covering events
        from standalone subsystems so you can exercise your handler for every
        production webhook.

        Supported types are the `fax.*` and `queue.*` webhook events plus
        `recording.failed` and `call.mobile_push_wakeup`. An unrecognized type
        returns `400` with the current supported list. Available only with a
        test key (`sk_test_...`); a live key receives a `400`.
      operationId: createTestEvent
      tags: [Webhook Endpoints]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  description: Webhook event type to emit.
                  example: 'queue.call.answered'
              required:
                - event
      responses:
        '200':
          description: Event emitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestEventResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/calls:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Initiate a call
      description: |
        Initiates an outbound call to a destination by first establishing a connection
        with the specified user.

        **Call Flow:**
        1. The system rings all of the user's registered devices simultaneously
        2. When the user answers on any device, the system dials the destination
        3. Both legs are bridged together into a single call

        The `dial_string` can be:
        - A phone number in E.164 format (e.g., `+15551234567`)
        - A local phone number (normalized based on account region)
        - An internal extension
        - An emergency number (e.g., `911`)

        This is an asynchronous operation. A `202 Accepted` response indicates the call
        request was queued successfully, not that the call has completed or even started.

      operationId: createCall
      tags: [Calls]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCallRequest'
            examples:
              pstn:
                summary: Call a phone number
                value:
                  user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                  dial_string: '+15551234567'
              local:
                summary: Call a local number
                value:
                  user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                  dial_string: '5551234567'
              extension:
                summary: Call an extension
                value:
                  user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                  dial_string: '1001'
      responses:
        '202':
          description: Call initiated successfully (queued for processing)
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UserNotFound'
    get:
      summary: List call logs
      description: |
        List call history for an account with optional filtering.
        Results are returned in reverse chronological order (newest first).

      operationId: listCallLogs
      tags: [Calls]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: user_id
          in: query
          description: Filter by specific user
          schema:
            type: string
        - name: endpoint_id
          in: query
          description: Filter by specific endpoint/device
          schema:
            type: string
        - name: direction
          in: query
          description: Filter by call direction
          schema:
            type: string
            enum: [inbound, outbound, internal]
        - name: from_number
          in: query
          description: Filter by caller's phone number (exact match)
          schema:
            type: string
            maxLength: 20
        - name: to_number
          in: query
          description: Filter by the call's destination (exact match). Matches either the number as dialed (an E.164 phone number for inbound calls) or the routed destination reported in the call's `to_number` (an extension number for calls that terminate inside the account).
          schema:
            type: string
            maxLength: 20
        - name: status
          in: query
          description: Filter by call status
          schema:
            type: string
            enum: [completed, no-answer, busy, failed, voicemail]
        - name: from_date
          in: query
          description: Return calls started on or after this date (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: to_date
          in: query
          description: Return calls started before this date (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `did`.'
          schema:
            type: array
            items:
              type: string
              enum: [did]
      responses:
        '200':
          description: List of call logs
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CallLog'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                list:
                  summary: Call log list response
                  value:
                    object: 'list'
                    url: '/v1/calls'
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - id: 'call_01h2xcejqtf2nbrexx3vqjhp45'
                        user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                        endpoint: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
                        did: 'did_01h2xcejqtf2nbrexx3vqjhp46'
                        direction: 'inbound'
                        from_number: '+14155551234'
                        to_number: '1002'
                        started_at: '2025-10-18T14:30:00Z'
                        answered_at: '2025-10-18T14:30:05Z'
                        connected_at: '2025-10-18T14:31:02Z'
                        ended_at: '2025-10-18T14:35:30Z'
                        duration_seconds: 325
                        status: 'completed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/calls/{call_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/CallId'
    get:
      summary: Get call log details
      description: |
        Retrieve a specific call log by ID.

      operationId: getCallLog
      tags: [Calls]
      parameters:
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `did`.'
          schema:
            type: array
            items:
              type: string
              enum: [did]
      responses:
        '200':
          description: Call log details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CallLog'
              examples:
                completed_call:
                  summary: Completed inbound call
                  value:
                    id: 'call_01h2xcejqtf2nbrexx3vqjhp45'
                    user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                    endpoint: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
                    did: 'did_01h2xcejqtf2nbrexx3vqjhp46'
                    direction: 'inbound'
                    from_number: '+14155551234'
                    to_number: '1002'
                    started_at: '2025-10-18T14:30:00Z'
                    answered_at: '2025-10-18T14:30:05Z'
                    connected_at: '2025-10-18T14:31:02Z'
                    ended_at: '2025-10-18T14:35:30Z'
                    duration_seconds: 325
                    status: 'completed'
                missed_call:
                  summary: Missed call (no answer)
                  value:
                    id: 'call_01h2xcejqtf2nbrexx3vqjhp47'
                    user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                    did: 'did_01h2xcejqtf2nbrexx3vqjhp46'
                    direction: 'inbound'
                    from_number: '+14155551234'
                    to_number: '1002'
                    started_at: '2025-10-18T15:00:00Z'
                    ended_at: '2025-10-18T15:00:30Z'
                    status: 'no-answer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/CallNotFound'
    post:
      summary: Control an active call
      description: |
        Send actions to control an active call. This is the central API for all call
        control operations such as attaching audio streams or transferring calls.

        Actions are processed sequentially. Each action may block processing or allow
        it to continue based on its outcome (see action descriptions for details).

        **Replacing actions:** If the call is currently executing actions and you send
        a new update, the previous action list is completely replaced. Processing of
        the current action is interrupted immediately, and processing resumes with the
        first action in the new list.

        **Example - Attach AI voice and fallback to human:**
        ```json
        {
          "actions": [
            {"type": "attach", "url": "wss://ai.example.com/voice"},
            {"type": "transfer", "target": "100"}
          ]
        }
        ```
        In this example, the AI handles the call via WebSocket. When the WebSocket
        disconnects, the call transfers to extension 100.

      operationId: updateCall
      tags: [Calls]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCallRequest'
            examples:
              attach_ai:
                summary: Attach AI voice session
                value:
                  actions:
                    - type: 'attach'
                      url: 'wss://ai.platform.example.com/voice'
              transfer:
                summary: Transfer to extension
                value:
                  actions:
                    - type: 'transfer'
                      target: '100'
              attach_then_transfer:
                summary: AI with human fallback
                value:
                  actions:
                    - type: 'attach'
                      url: 'wss://ai.platform.example.com/voice'
                    - type: 'transfer'
                      target: '100'
      responses:
        '202':
          description: Actions accepted for processing
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/CallNotFound'

  /v1/calls/{call_id}/transcript:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/CallId'
    get:
      summary: Get call transcript
      description: |
        Retrieve the transcript for a specific call.

        **Transcription Status:**
        - `pending`: The call has been recorded but transcription has not started
        - `processing`: Transcription is currently in progress
        - `completed`: Transcription is complete and text is available
        - `failed`: Transcription failed (text will be null)

        **Note:** Returns 404 if the call exists but has no recording, or if the call
        does not exist. Not all calls have recordings (depends on account configuration).

      operationId: getCallTranscript
      tags: [Calls]
      responses:
        '200':
          description: Call transcript
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transcript'
              examples:
                completed:
                  summary: Completed transcription
                  value:
                    call_id: 'call_01h2xcejqtf2nbrexx3vqjhp45'
                    status: 'completed'
                    text: 'Hello, this is Dr. Smith calling...'
                pending:
                  summary: Pending transcription
                  value:
                    call_id: 'call_01h2xcejqtf2nbrexx3vqjhp45'
                    status: 'pending'
                    text: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/TranscriptNotFound'

  /v1/calls/{call_id}/recording:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/CallId'
    get:
      summary: Get call recording
      description: |
        Retrieve the recording for a specific call. Returns metadata and a signed download URL.

        The download URL is valid for 10 minutes. After expiration, request a new URL by calling this endpoint again.

        **Note:** Returns 404 if the call exists but has no recording, or if the call
        does not exist. Not all calls have recordings (depends on account configuration).

      operationId: getCallRecording
      tags: [Calls]
      responses:
        '200':
          description: Call recording
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Recording'
              examples:
                recording:
                  summary: Recording with metadata
                  value:
                    call_id: 'call_01h2xcejqtf2nbrexx3vqjhp45'
                    duration_seconds: 185
                    file_size_bytes: 2960000
                    download_url: 'https://cdn.example.com/recordings/2026-03-05/call.wav?Expires=1709654400&Signature=abc123'
                    expires_at: '2026-03-05T15:40:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/RecordingNotFound'

  /v1/calls/{call_id}/recording/pause:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/CallId'
    post:
      summary: Pause call recording
      description: |
        Pause recording on an active call — for example, while an agent collects
        a customer's payment card number so the sensitive audio is never captured.

        Recording stays paused (the recording file continues with silence for the
        paused span) until you call the resume endpoint or the call ends. Both
        parties hear a short confirmation tone. Resume with
        `POST /v1/calls/{call_id}/recording/resume`.

        The request is accepted asynchronously (`202`). Returns `404` if the call
        is not active for the selected account. If the active call has no recording
        in progress, the command is a no-op.
      operationId: pauseCallRecording
      tags: [Calls]
      responses:
        '202':
          description: Pause command accepted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/calls/{call_id}/recording/resume:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/CallId'
    post:
      summary: Resume call recording
      description: |
        Resume a recording previously paused with
        `POST /v1/calls/{call_id}/recording/pause`. Both parties hear a short
        confirmation tone.

        The request is accepted asynchronously (`202`). Returns `404` if the call
        is not active for the selected account. If the active call has no recording
        in progress, the command is a no-op.
      operationId: resumeCallRecording
      tags: [Calls]
      responses:
        '202':
          description: Resume command accepted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/calls/{call_id}/listeners:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/CallId'
    post:
      summary: Create a listener
      description: |
        Start streaming real-time audio from an active call to a WebSocket URL.

        DialStack opens an outbound WebSocket connection to the specified URL and
        streams audio unidirectionally (DialStack to your server). The call is not
        affected — both parties remain unaware of the listener.

        Use the `channel` parameter to select which audio to receive:
        - `caller` — audio from the party that initiated the call
        - `callee` — audio from the party that received the call
        - `both` — both channels, delivered as separate tagged messages

        The listener automatically stops when the call ends. You can also stop it
        explicitly with `DELETE /v1/calls/{call_id}/listeners/{listener_id}`.

        Returns `409 Conflict` if the call is not in a state that supports listeners
        (e.g., still ringing or already ended). Retry after the call is answered.

        See the [WebSocket API](/websocket-api) for the listener message protocol.

      operationId: createListener
      tags: [Calls]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateListenerRequest'
            examples:
              both_channels:
                summary: Listen to both sides of the call
                value:
                  url: 'wss://your-server.example.com/audio'
                  channel: 'both'
              caller_only:
                summary: Listen to caller audio only
                value:
                  url: 'wss://your-server.example.com/audio'
                  channel: 'caller'
      responses:
        '201':
          description: Listener created and WebSocket connection initiated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Listener'
              examples:
                listener:
                  value:
                    id: 'lstn_01h2xcejqtf2nbrexx3vqjhp50'
                    call_id: 'call_01h2xcejqtf2nbrexx3vqjhp45'
                    url: 'wss://your-server.example.com/audio'
                    channel: 'both'
                    created_at: '2026-04-02T14:30:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/CallNotFound'
        '409':
          $ref: '#/components/responses/CallNotActive'
        '422':
          description: |
            Request body failed field validation. Examples:
            - `url is required` — the field was omitted or empty
            - `url must be a secure WebSocket URL (wss://)` — insecure scheme rejected
            - `channel must be one of caller, callee, both` — unsupported channel value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      summary: List listeners
      description: |
        List the listeners currently attached to a call. Listeners exist
        only for the duration of the call — once the call ends they no
        longer exist, and this endpoint returns an empty list. Call IDs
        that don't belong to the caller's account return 404.

      operationId: listListeners
      tags: [Calls]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of active listeners
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Listener'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/CallNotFound'

  /v1/calls/{call_id}/listeners/{listener_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/CallId'
      - $ref: '#/components/parameters/ListenerId'
    get:
      summary: Get listener
      description: |
        Retrieve a listener. Listeners exist only for the duration of
        the call; once the call ends the listener no longer exists and
        this endpoint returns 404.

      operationId: getListener
      tags: [Calls]
      responses:
        '200':
          description: Listener details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Listener'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ListenerNotFound'
    delete:
      summary: Stop a listener
      description: |
        Stop streaming audio and close the WebSocket connection for this listener.
        The call is not affected. Listeners exist only for the duration of
        the call, so once the call has ended this endpoint returns 404.

      operationId: deleteListener
      tags: [Calls]
      responses:
        '204':
          description: Listener stopped
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ListenerNotFound'

  # Phone Numbers

  /v1/phone-numbers:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    get:
      summary: List phone numbers
      description: |
        List all phone numbers assigned to the account.
        Results are returned in reverse chronological order (newest first).

      operationId: listPhoneNumbers
      tags: [Phone Numbers]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/PhoneNumberStatus'
      responses:
        '200':
          description: List of phone numbers
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PhoneNumber'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                list:
                  summary: Phone number list response
                  value:
                    object: 'list'
                    url: '/v1/phone-numbers'
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - id: 'did_01h2xcejqtf2nbrexx3vqjhp46'
                        phone_number: '+14155551234'
                        status: 'active'
                        outbound_enabled: true
                        routing_target: null
                        created_at: '2025-10-18T10:00:00Z'
                        updated_at: '2025-10-18T10:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/phone-numbers/{phone_number_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PhoneNumberId'

    get:
      summary: Get a phone number
      description: |
        Retrieve a phone number by ID.

      operationId: getPhoneNumber
      tags: [Phone Numbers]
      responses:
        '200':
          description: Phone number details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumber'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PhoneNumberNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      summary: Update a phone number
      description: |
        Update properties of a phone number.

      operationId: updatePhoneNumber
      tags: [Phone Numbers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                outbound_enabled:
                  type: boolean
                  description: Whether this phone number can be used for outbound calling
                status:
                  type: string
                  enum: [active, inactive]
                  description: >-
                    Set to `inactive` to stop routing inbound calls to this number,
                    or `active` to resume routing. Setting to `active` on a released
                    number triggers re-acquisition from the carrier; this fails with
                    409 if the number is no longer available.
                expires_at:
                  type: ['string', 'null']
                  format: date-time
                  description: >-
                    Set to `null` to convert a temporary number into a permanent one
                    (cancels the scheduled release). Extending the expiry is not supported.
                directory_listing_name:
                  type: string
                  description: Business name for the directory listing
                directory_listing_type:
                  type: string
                  enum: [listed, non_listed, non_published, non_registered]
                  description: >-
                    How the number appears in directory services. Setting to a non-registered
                    type removes any existing listing. Setting to listed, non_listed, or
                    non_published submits a directory listing order.
                directory_listing_location:
                  type: string
                  description: >-
                    ID of the location whose address to use for the listing.
                    Required when directory_listing_type is not non_registered.
                caller_id_name:
                  type: string
                  maxLength: 15
                  description: >-
                    Caller ID (CNAM) display name. Max 15 ASCII characters per the CNAM standard.
                    Submits an asynchronous order to the upstream provider. The phone number must
                    be in `active` status.
                  example: 'ACME Corp'
                caller_id_prefix:
                  type: ['string', 'null']
                  maxLength: 32
                  description: >-
                    Prefix added to the inbound caller's display name shown on
                    the answering device (e.g. `[Acme] John Smith`), for calls
                    answered on behalf of a specific brand or business. Applies
                    only to what the answering party sees on inbound calls and
                    does not change call records. Unlike `caller_id_name`, this is
                    stored directly with no upstream order. Send `null` to clear.
                  example: '[Acme]'
                caller_id_visibility:
                  type: string
                  enum: [PUBLIC, PRIVATE]
                  default: 'PUBLIC'
                  description: Whether the caller ID name is publicly visible
                fax_enabled:
                  type: boolean
                  description: >-
                    Set to `true` to make this number fax-only on inbound: incoming
                    calls are received as faxes and the inbound routing target is
                    bypassed (preserved, and resumes when set back to `false`).
                config:
                  allOf:
                    - $ref: '#/components/schemas/DIDConfig'
                  description: >-
                    Per-number configuration. Replaces the whole config blob, so send
                    the full `fax_notifications` object when updating recipients
                    or `delete_documents`. Pass `null` to clear it.
            examples:
              disable_outbound:
                summary: Disable outbound calling
                value:
                  outbound_enabled: false
              enable_fax:
                summary: Enable fax with notification recipients
                value:
                  fax_enabled: true
                  config:
                    fax_notifications:
                      recipients: ['fax@example.com']
                      delete_documents: false
              deactivate:
                summary: Deactivate a phone number
                value:
                  status: inactive
              keep_temp_number:
                summary: Convert a temporary number to permanent
                value:
                  expires_at: null
              recover_released_number:
                summary: Recover a previously released number
                value:
                  status: active
              set_caller_id:
                summary: Set caller ID display name
                value:
                  caller_id_name: 'ACME Corp'
      responses:
        '200':
          description: Updated phone number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumber'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PhoneNumberNotFound'
        '409':
          description: A caller ID update is already in progress for this number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: The upstream provider rejected the caller ID or directory listing update
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '502':
          description: Upstream provider error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Caller ID service is not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # Available Phone Numbers

  /v1/available-phone-numbers:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    get:
      summary: Search available phone numbers
      description: |
        Search for phone numbers available for purchase.
        At least one search filter is required: `area_code`, `state`, `zip`, `npa_nxx`, or `city` combined with `state`.
        Filters can be combined to narrow results.

        A well-formed ZIP code may still be rejected if it is unknown or
        PO Box–only (for example `85001` or `22313`); this returns a `400`
        with an actionable error message rather than an empty result list.

      operationId: searchAvailablePhoneNumbers
      tags: [Phone Numbers]
      parameters:
        - name: area_code
          in: query
          required: false
          description: Three-digit area code to search
          schema:
            type: string
            pattern: '^\d{3}$'
            example: '919'
        - name: city
          in: query
          required: false
          description: City name (requires `state`)
          schema:
            type: string
            example: 'Raleigh'
        - name: state
          in: query
          required: false
          description: Two-letter state abbreviation (can be used alone or with `city`)
          schema:
            type: string
            pattern: '^[A-Z]{2}$'
            example: 'NC'
        - name: zip
          in: query
          required: false
          description: Five-digit US ZIP code
          schema:
            type: string
            pattern: '^\d{5}$'
            example: '27601'
        - name: npa_nxx
          in: query
          required: false
          description: Six-digit NPA-NXX prefix (area code + exchange)
          schema:
            type: string
            pattern: '^\d{6}$'
            example: '919555'
        - name: quantity
          in: query
          required: false
          description: Number of results to return (1-100, default 10)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
      responses:
        '200':
          description: List of available phone numbers
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AvailablePhoneNumber'
                required:
                  - data
              examples:
                results:
                  summary: Available numbers found
                  value:
                    data:
                      - phone_number: '+19195551234'
                        city: 'RALEIGH'
                        state: 'NC'
                        rate_center: 'RALEIGH'
                        lata: '422'
                      - phone_number: '+19195555678'
                        city: 'RALEIGH'
                        state: 'NC'
                        rate_center: 'RALEIGH'
                        lata: '422'
                empty:
                  summary: No numbers available
                  value:
                    data: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '502':
          $ref: '#/components/responses/BadGateway'

  # Phone Number Orders

  /v1/phone-number-orders:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Order new phone numbers
      description: |
        Order one or more new phone numbers. Numbers must have been found via the
        search endpoint (`GET /v1/available-phone-numbers`). This endpoint is for
        acquiring new numbers only — it does not support porting existing numbers
        from another provider.

        The order is created in `pending` status and typically completes within
        seconds. Poll the order via `GET /v1/phone-number-orders/{order_id}` or
        configure a webhook to be notified when it reaches a terminal status
        (`complete`, `partial`, or `failed`).

      operationId: createNumberOrder
      tags: [Phone Numbers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateNumberOrderRequest'
            examples:
              basic:
                summary: Order two numbers
                value:
                  phone_numbers:
                    - '+19195551234'
                    - '+19195555678'
      responses:
        '201':
          description: Order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NumberOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      summary: List phone number orders
      description: |
        Returns a paginated list of phone number orders for the account.
        Includes both purchase and disconnect orders. Results are returned
        in reverse chronological order (newest first).

      operationId: listNumberOrders
      tags: [Phone Numbers]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/NumberOrderStatus'
        - $ref: '#/components/parameters/NumberOrderType'
      responses:
        '200':
          description: List of phone number orders
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/NumberOrder'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/phone-number-orders/{order_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/OrderId'

    get:
      summary: Get a phone number order
      description: |
        Retrieves a phone number order by ID. If the order is still pending,
        the status is refreshed from the provider before returning.

      operationId: getNumberOrder
      tags: [Phone Numbers]
      responses:
        '200':
          description: Order details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NumberOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NumberOrderNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # Phone Number Disconnect

  /v1/phone-numbers/{phone_number_id}/disconnect:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PhoneNumberId'

    post:
      summary: Disconnect a phone number
      description: |
        Release a phone number back to the provider. Creates a disconnect order
        that transitions the phone number to `released` status on completion.

      operationId: disconnectPhoneNumber
      tags: [Phone Numbers]
      responses:
        '201':
          description: Disconnect order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NumberOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PhoneNumberNotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/phone-numbers/{phone_number_id}/reassign:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PhoneNumberId'

    post:
      summary: Reassign a phone number
      description: |
        Move a phone number to a different account.

        On reassignment, the routing target is cleared and outbound calling is disabled.
        The phone number must be in `active` or `inactive` status.

      operationId: reassignPhoneNumber
      tags: [Phone Numbers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                target_account:
                  type: string
                  description: ID of the account to reassign the phone number to
              required:
                - target_account
            examples:
              reassign:
                summary: Reassign to another account
                value:
                  target_account: 'acct_01h2xcejqtf2nbrexx3vqjhp47'
      responses:
        '200':
          description: Reassigned phone number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumber'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PhoneNumberNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/phone-numbers/{phone_number_id}/route:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PhoneNumberId'

    post:
      summary: Update inbound routing
      description: |
        Set how inbound calls to a phone number are handled.

        - To route calls to a [routing target](#section/Routing-Targets), send
          the target's ID in `routing_target`. Send `null` to clear it.
        - To deliberately stop inbound calls (no ring, no message), send
          `inbound_routing: "drop"`. `routing_target` must be null or omitted;
          sending a target alongside `drop` is rejected. Dropping is rejected on
          a fax-enabled number (fax takes precedence) — disable fax first.

        Omitting `routing_target` in `default` mode returns the current phone
        number state unchanged.

      operationId: updatePhoneNumberRouting
      tags: [Phone Numbers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                routing_target:
                  type: [string, 'null']
                  description: 'ID of the target to route inbound calls to; null clears it. To deliberately stop inbound calls, use inbound_routing "drop" rather than clearing. Omit to leave routing unchanged.'
                inbound_routing:
                  type: string
                  enum: [default, drop]
                  description: >-
                    `default` follows `routing_target`; `drop` drops inbound
                    calls. Defaults to `default` when omitted.
            examples:
              set_target:
                summary: Route calls to a target
                value:
                  routing_target: 'user_01h2xcejqtf2nbrexx3vqjhp48'
              clear_target:
                summary: Clear the routing target
                value:
                  routing_target: null
              drop:
                summary: Drop inbound calls
                value:
                  inbound_routing: 'drop'
      responses:
        '200':
          description: Updated phone number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumber'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PhoneNumberNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/phone-numbers/{phone_number_id}/sms-port-out:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PhoneNumberId'

    get:
      summary: Get the SMS port-out window
      description: |
        Returns the number's SMS port-out window. While the window is open
        (`expires_at` is a future timestamp), an inbound SMS port-out (messaging
        transfer) request for this number is authorized automatically; otherwise
        it is denied. `expires_at` is `null` when the window is locked.
      operationId: getSmsPortOut
      tags: [Phone Numbers]
      responses:
        '200':
          description: The SMS port-out window
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmsPortOutWindow'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PhoneNumberNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      summary: Open or lock the SMS port-out window
      description: |
        Set the number's SMS port-out window. Send a future `expires_at`
        timestamp to open the window until that instant, or `null` to lock it.
        `expires_at` is required (there is no default); a timestamp in the past,
        or more than 365 days in the future, is rejected.
      operationId: updateSmsPortOut
      tags: [Phone Numbers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SmsPortOutWindow'
            examples:
              open:
                summary: Open the window until a future instant
                value:
                  expires_at: '2026-06-29T09:30:00Z'
              lock:
                summary: Lock the window
                value:
                  expires_at: null
      responses:
        '200':
          description: The updated SMS port-out window
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmsPortOutWindow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PhoneNumberNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # Port-In Eligibility (no DialStack-Account header required)

  /v1/port-in-eligibility:
    post:
      summary: Check port-in eligibility
      description: |
        Check whether phone numbers are eligible for porting to DialStack. Returns
        carrier information for portable numbers and rate center details for
        non-portable numbers.

        This is a pre-purchase check and does not require an account context.

      operationId: checkPortInEligibility
      tags: [Number Porting]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PortInEligibilityRequest'
            examples:
              basic:
                summary: Check two numbers
                value:
                  phone_numbers:
                    - '+12025551234'
                    - '+14155559876'
      responses:
        '200':
          description: Eligibility check result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortInEligibilityResult'
              examples:
                success:
                  summary: Mixed eligibility result
                  value:
                    portable_numbers:
                      - phone_number: '+14155559876'
                        losing_carrier_name: 'T-Mobile US-SVR-10X/2'
                        losing_carrier_spid: '6529'
                        is_wireless: true
                        account_number_required: true
                    non_portable_numbers:
                      - phone_number: '+12025551234'
                        rate_center: 'WASHINGT DC'
                        city: 'Washington'
                        state: 'DC'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # Number Porting

  /v1/port-orders:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Create a draft port order
      description: |
        Create a new port order in `draft` status. After creating the draft, approve
        the order with an electronic signature, then submit it to begin the porting process.

        Phone numbers are validated and normalized to E.164 format. Toll-free numbers
        are not supported.

      operationId: createPortOrder
      tags: [Number Porting]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePortOrderRequest'
            examples:
              basic:
                summary: Port a single number
                value:
                  phone_numbers:
                    - '+12025551234'
                  subscriber:
                    btn: '+12025551234'
                    business_name: 'Doe Enterprises'
                    approver_name: 'John Doe'
                    address:
                      house_number: '123'
                      street_name: 'Main St'
                      city: 'Anytown'
                      state: 'VA'
                      zip: '22030'
                  requested_foc_date: '2026-03-01'
      responses:
        '201':
          description: Draft port order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      summary: List port orders
      description: |
        Returns a paginated list of port orders for the account.
        Results are returned in reverse chronological order (newest first).
        Optionally filter by status.

      operationId: listPortOrders
      tags: [Number Porting]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/PortOrderStatus'
      responses:
        '200':
          description: List of port orders
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PortOrder'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/port-orders/{order_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PortOrderId'

    get:
      summary: Get a port order
      description: |
        Retrieve a port order by ID. If the order is in a non-terminal status
        (`submitted`, `exception`, or `foc`), the status is automatically refreshed
        from the carrier before returning.

      operationId: getPortOrder
      tags: [Number Porting]
      responses:
        '200':
          description: Port order details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PortOrderNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      summary: Update a port order
      description: |
        Update a port order that is in `approved` or `exception` status. Use this to correct
        information that caused a rejection, then resubmit. All fields are optional —
        only provided fields are updated. Updating an approved order clears the approval
        and reverts the status to `draft`.

      operationId: updatePortOrder
      tags: [Number Porting]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePortOrderRequest'
            examples:
              fix_address:
                summary: Fix service address after rejection
                value:
                  subscriber:
                    btn: '+12025551234'
                    business_name: 'Doe Enterprises'
                    approver_name: 'John Doe'
                    address:
                      house_number: '456'
                      street_name: 'Oak Ave'
                      city: 'Anytown'
                      state: 'VA'
                      zip: '22030'
      responses:
        '200':
          description: Port order updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PortOrderNotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/port-orders/{order_id}/submit:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PortOrderId'

    post:
      summary: Submit a port order
      description: |
        Submit an approved port order for processing. The order must first be
        [approved](#tag/Number-Porting/operation/approvePortOrder) by the customer.

        On success, the order status transitions from `approved` to `submitted`.
        The necessary authorization documents are generated and forwarded to the
        carrier automatically.

      operationId: submitPortOrder
      tags: [Number Porting]
      responses:
        '200':
          description: Port order submitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PortOrderNotFound'
        '409':
          description: Order is not in approved status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Approval required — the order must be approved before submission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/port-orders/{order_id}/cancel:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PortOrderId'

    post:
      summary: Cancel a port order
      description: |
        Cancel a port order. Draft and approved orders are cancelled locally.
        Submitted orders are also cancelled with the carrier.

        Cannot cancel orders in terminal status (`complete` or `cancelled`).

      operationId: cancelPortOrder
      tags: [Number Porting]
      responses:
        '200':
          description: Port order cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PortOrderNotFound'
        '409':
          description: Order is in a terminal status and cannot be cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/port-orders/{order_id}/approve:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PortOrderId'

    post:
      summary: Approve a port order
      description: |
        Have the customer approve a port order by providing their electronic signature
        and IP address. This authorizes the number transfer and allows the order to be submitted.

        The order must be in `draft` status and must have subscriber details.

        On success, the order status transitions from `draft` to `approved`. If the order
        details are updated after approval, the approval is cleared and the status reverts
        to `draft` — the customer must approve again.

      operationId: approvePortOrder
      tags: [Number Porting]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApprovePortOrderRequest'
      responses:
        '200':
          description: Port order approved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PortOrderNotFound'
        '409':
          description: Order is not in draft status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Subscriber details are missing from the port order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/port-orders/{order_id}/csr:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PortOrderId'

    post:
      summary: Upload CSR document
      description: |
        Upload an optional Customer Service Record (CSR) document for a port order.
        A CSR can help resolve porting issues by providing carrier account details.

        Accepted formats: PDF, PNG, JPG, TIFF. Maximum file size: 3 MB.

      operationId: uploadPortOrderCSR
      tags: [Number Porting]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: CSR document file
              required:
                - file
      responses:
        '200':
          description: CSR uploaded successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'csr uploaded successfully'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PortOrderNotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      summary: Download CSR document
      description: |
        Download the Customer Service Record (CSR) document for a port order.

      operationId: downloadPortOrderCSR
      tags: [Number Porting]
      responses:
        '200':
          description: CSR document file
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
            image/tiff:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/DocumentNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/port-orders/{order_id}/bill-copy:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PortOrderId'

    post:
      summary: Upload bill copy document
      description: |
        Upload an optional bill copy document for a port order.
        A bill copy can help resolve porting issues by providing carrier account details.

        Accepted formats: PDF, PNG, JPG, TIFF. Maximum file size: 3 MB.

      operationId: uploadPortOrderBillCopy
      tags: [Number Porting]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: Bill copy document file
              required:
                - file
      responses:
        '200':
          description: Bill copy uploaded successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'bill_copy uploaded successfully'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PortOrderNotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      summary: Download bill copy document
      description: |
        Download the bill copy document for a port order.

      operationId: downloadPortOrderBillCopy
      tags: [Number Porting]
      responses:
        '200':
          description: Bill copy document file
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
            image/tiff:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/DocumentNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v1/port-orders/{order_id}/events:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/PortOrderId'

    get:
      summary: List port order events
      description: |
        Returns the audit trail of status changes for a port order,
        in reverse chronological order.

      operationId: listPortOrderEvents
      tags: [Number Porting]
      responses:
        '200':
          description: List of port order events
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PortOrderEvent'
                required:
                  - object
                  - data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PortOrderNotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # Voice Apps

  # AI Agents

  /v1/ai-agents:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Create an AI agent
      description: |
        Create a new AI agent within the account. This automatically provisions
        a managed voice app and extension for call routing.

      operationId: createAIAgent
      tags: [AI Agents]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAIAgentRequest'
            examples:
              basic:
                summary: Create a receptionist agent
                value:
                  name: 'Front Desk Receptionist'
                  extension_number: '200'
                  instructions: 'You are the receptionist for Jones Family Dental.'
                  faq_responses:
                    - question: 'What are your hours?'
                      answer: 'Monday through Friday, 9 AM to 5 PM.'
      responses:
        '201':
          description: AI agent created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AIAgent'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Extension number already in use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      summary: List AI agents
      description: |
        List all AI agents for the account.
        Results are returned in reverse chronological order (newest first).

      operationId: listAIAgents
      tags: [AI Agents]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of AI agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AIAgent'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/ai-agents/{ai_agent_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/AIAgentId'
    get:
      summary: Get AI agent details
      description: |
        Retrieve a specific AI agent by ID.

      operationId: getAIAgent
      tags: [AI Agents]
      responses:
        '200':
          description: AI agent details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AIAgent'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/AIAgentNotFound'
    post:
      summary: Update AI agent
      description: |
        Update AI agent details (name, instructions, or FAQ responses).

      operationId: updateAIAgent
      tags: [AI Agents]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAIAgentRequest'
      responses:
        '200':
          description: AI agent updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AIAgent'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/AIAgentNotFound'
    delete:
      summary: Delete AI agent
      description: |
        Delete an AI agent and its managed voice app and extension.
        This operation is irreversible.

      operationId: deleteAIAgent
      tags: [AI Agents]
      responses:
        '204':
          $ref: '#/components/responses/AIAgentDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/AIAgentNotFound'

  /v1/voice-apps:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Create a voice app
      description: |
        Create a new voice app within the account. Voice apps handle calls via HTTP
        webhook notification and optional WebSocket audio streaming.

      operationId: createVoiceApp
      tags: [Voice Apps]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateVoiceAppRequest'
            examples:
              basic:
                summary: Create AI receptionist
                value:
                  name: 'AI Receptionist'
                  url: 'https://ai.platform.example.com/calls'
      responses:
        '201':
          description: Voice app created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceApp'
              examples:
                success:
                  summary: Successful voice app creation
                  value:
                    id: 'va_01h2xcejqtf2nbrexx3vqjhp49'
                    name: 'AI Receptionist'
                    url: 'https://ai.platform.example.com/calls'
                    status: 'active'
                    secret: 'whsec_abc123def456...'
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    get:
      summary: List voice apps
      description: |
        List all voice apps for the account.
        Results are returned in reverse chronological order (newest first).

      operationId: listVoiceApps
      tags: [Voice Apps]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: List of voice apps
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/VoiceApp'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                list:
                  summary: Voice app list response
                  value:
                    object: 'list'
                    url: '/v1/voice-apps'
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - id: 'va_01h2xcejqtf2nbrexx3vqjhp49'
                        name: 'AI Receptionist'
                        url: 'https://ai.platform.example.com/calls'
                        status: 'active'
                        secret: 'whsec_abc123def456...'
                        created_at: '2025-10-18T10:00:00Z'
                        updated_at: '2025-10-18T10:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/voice-apps/{voice_app_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/VoiceAppId'
    get:
      summary: Get voice app details
      description: |
        Retrieve a specific voice app by ID.

      operationId: getVoiceApp
      tags: [Voice Apps]
      parameters:
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: Voice app details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceApp'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/VoiceAppNotFound'
    post:
      summary: Update voice app
      description: |
        Update voice app details (name, URL, or status).

      operationId: updateVoiceApp
      tags: [Voice Apps]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateVoiceAppRequest'
      responses:
        '200':
          description: Voice app updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceApp'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/VoiceAppNotFound'
    delete:
      summary: Delete voice app
      description: |
        Delete a voice app. Active calls to this voice app will be terminated.
        This operation is irreversible.

      operationId: deleteVoiceApp
      tags: [Voice Apps]
      responses:
        '204':
          $ref: '#/components/responses/VoiceAppDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/VoiceAppNotFound'

  /v1/voice-apps/{voice_app_id}/rotate_secret:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/VoiceAppId'
    post:
      summary: Rotate webhook secret
      description: |
        Generate a new webhook signing secret for the voice app.
        The old secret is immediately invalidated.

      operationId: rotateVoiceAppSecret
      tags: [Voice Apps]
      responses:
        '200':
          description: Secret rotated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceApp'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/VoiceAppNotFound'

  # Real-Time Events (SSE)

  /v1/events:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    get:
      summary: Stream real-time events
      description: |
        Establish a Server-Sent Events (SSE) connection to receive real-time notifications
        about account activity such as incoming calls.

        **Connection Behavior:**
        - On successful connection, a `connected` event is sent immediately
        - Events are streamed in real-time as they occur
        - The connection stays open indefinitely until the client disconnects
        - Clients should implement automatic reconnection with exponential backoff

        **Event Format:**
        Events follow the SSE specification with `event:` and `data:` fields:
        ```
        event: call.incoming
        data: {"event":"call.incoming","account_id":"acct_...","from_number":"+14155551234","to_number":"+14155559876"}
        ```

        **Available Event Types:**

        ### connected
        Sent immediately when the SSE connection is established.
        ```json
        {"message": "Connected to event stream"}
        ```

        ### call.incoming
        Sent when an incoming call arrives for the account.

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.incoming"` |
        | `account` | string | Account receiving the call |
        | `call_id` | string | Call detail record identifier |
        | `from_number` | string | Caller's phone number (E.164) |
        | `from_name` | string \| null | Caller's name from caller ID |
        | `to_number` | string | Called phone number (E.164) |
        | `from_user` | string \| null | User who initiated the call, when known |
        | `to_user` | string \| null | User the call is destined for, when known |

        ### call.initiated
        Sent when an outbound call starts dialing.

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.initiated"` |
        | `account` | string | Account initiating the call |
        | `call_id` | string | Call detail record identifier |
        | `from_number` | string | Outbound caller number (E.164) |
        | `to_number` | string | Destination number (E.164) |
        | `user` | string | User who initiated the call (alias for `from_user`) |
        | `from_user` | string \| null | User who initiated the call, when known |
        | `to_user` | string \| null | User the call is destined for, when known |

        ### call.ringing
        Sent once per (call, user) when the platform is about to ring a user's phone(s). Fires
        regardless of how the call was routed to the user — direct extension, ring group, ring-all-users,
        Find-Me/Follow-Me (including forwarding to an external number), dial plan Dial User node, voice
        app transfer, or a parked-call ring-back. The event indicates logical "we're ringing this user",
        not a SIP 180 Ringing signal.

        A user with multiple endpoints (desk phone + mobile app) produces one event, not one per device.

        No stop event is emitted — consumers that dismiss UI on ring stop (e.g. screen pop) should rely on
        `call.answered` or `call.end`.

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.ringing"` |
        | `account` | string | Account owning the user being rung |
        | `call_id` | string | Call detail record identifier |
        | `user` | string | User whose phone(s) are being rung |
        | `from_number` | string | Caller's phone number (E.164) |
        | `from_name` | string \| null | Caller's name from caller ID |
        | `to_number` | string | Called phone number (E.164) |
        | `ringing_at` | string | ISO 8601 timestamp |

        ### call.mobile_push_wakeup
        Sent when an incoming call is being delivered to the calling session of a user who has
        `mobile_push_wakeup` enabled — the cue to send the push notification that wakes the user's
        mobile app. Unlike `call.ringing`, it never fires for pure external forwarding: every event
        corresponds to a real, answerable call for that user's app.

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.mobile_push_wakeup"` |
        | `account` | string | Account owning the user being woken |
        | `call_id` | string | Call detail record identifier |
        | `user` | string | User whose app should be woken |
        | `from_number` | string | Caller's phone number (E.164) |
        | `from_name` | string \| null | Caller's name from caller ID |
        | `to_number` | string | Called phone number (E.164) |
        | `ringing_at` | string | ISO 8601 timestamp |

        ### call.answered
        Sent when a call is answered.

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.answered"` |
        | `account` | string | Account where the call was answered |
        | `call_id` | string | Call detail record identifier |
        | `from_number` | string | Caller's phone number (E.164) |
        | `to_number` | string | Called phone number (E.164) |
        | `direction` | string | `"inbound"` or `"outbound"` |
        | `answered_at` | string | Signalling answer (a greeting/menu counts, so ≈ start for inbound calls that play one) |
        | `connected_at` | string \| null | When the winning leg (user device / external number) answered — live-conversation start. Null if answered by voicemail |
        | `from_user` | string \| null | User who initiated the call, when known |
        | `to_user` | string \| null | User who answered, when applicable |

        ### call.end
        Sent when a call ends.

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.end"` |
        | `account` | string | Account where the call ended |
        | `call_id` | string | Call detail record identifier |
        | `from_number` | string | Caller's phone number (E.164) |
        | `to_number` | string | Called phone number (E.164) |
        | `direction` | string | `"inbound"` or `"outbound"` |
        | `status` | string | `"completed"`, `"no-answer"`, `"busy"`, `"failed"`, or `"voicemail"` |
        | `duration_seconds` | integer | Call duration in seconds |
        | `answered_at` | string \| null | Signalling answer (a greeting/menu counts). Null if never answered |
        | `connected_at` | string \| null | When the winning leg (user device / external number) answered — live-conversation start. Null if abandoned while ringing/greeting or answered by voicemail |
        | `ended_at` | string | ISO 8601 timestamp |
        | `from_user` | string \| null | User who initiated the call, when known |
        | `to_user` | string \| null | User the call reached, when applicable |

        ### call.transfer
        Sent when a call is transferred. A warm (attended) transfer involves
        two calls, and `call.transfer` fires for each. The
        consultation call you placed is merged into the primary call when you
        complete the transfer — it is not hung up, so it never emits `call.end`;
        its `call.transfer` is its final event.

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.transfer"` |
        | `account` | string | Account where the transfer occurred |
        | `call_id` | string | Call detail record identifier |
        | `from_number` | string | Caller's phone number (E.164) |
        | `to_number` | string | Original called number (E.164) |
        | `transferred_to` | string | Transfer target (extension number or E.164 phone number) |
        | `related_call` | string \| null | The other call involved in an attended transfer, when it can be linked; `null` otherwise |

      operationId: streamEvents
      tags: [Events]
      responses:
        '200':
          description: SSE event stream established
          headers:
            Content-Type:
              schema:
                type: string
                example: 'text/event-stream'
            Cache-Control:
              schema:
                type: string
                example: 'no-cache'
            Connection:
              schema:
                type: string
                example: 'keep-alive'
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  Server-Sent Events stream. Each event has an `event:` type and `data:` payload.
              examples:
                connected:
                  summary: Connection established
                  value: |
                    event: connected
                    data: {"message":"Connected to event stream"}
                incoming_call:
                  summary: Incoming call event
                  value: |
                    event: call.incoming
                    data: {"event":"call.incoming","account_id":"acct_01h2xcejqtf2nbrexx3vqjhp41","from_number":"+14155551234","from_name":"John Smith","to_number":"+14155559876"}
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # Webhook Endpoints (Platform-level, no DialStack-Account header required)

  /v1/webhook_endpoints:
    post:
      summary: Create a webhook endpoint
      description: |
        Register a URL to receive event notifications. The endpoint's mode is
        determined by the API key used to create it: a live key creates a
        live-mode endpoint (`livemode: true`), a test key creates a sandbox
        endpoint (`livemode: false`). An endpoint only receives events from
        accounts in its own mode, so test traffic never reaches a live URL.

        The signing `secret` is returned **only** in this create response —
        store it to verify webhook signatures. It cannot be retrieved later.

        By default the endpoint is **platform-global** and receives every
        account's events for its mode. Include the `DialStack-Account` header to
        instead scope the endpoint to a single account: that account's events are
        then delivered to this endpoint **in addition** to the platform-global
        ones. The scope is fixed at creation.
      operationId: createWebhookEndpoint
      tags: [Webhook Endpoints]
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/DialStackAccountOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookEndpointRequest'
            examples:
              all_events:
                summary: Subscribe to all events
                value:
                  url: 'https://example.com/webhooks'
              filtered:
                summary: Subscribe to specific event types
                value:
                  url: 'https://example.com/webhooks'
                  enabled_events: ['call.answered', 'call.end']
                  description: 'Call lifecycle handler'
      responses:
        '201':
          description: Webhook endpoint created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
              examples:
                success:
                  summary: Created endpoint (secret shown once)
                  value:
                    id: 'we_01h2xcejqtf2nbrexx3vqjhp41'
                    url: 'https://example.com/webhooks'
                    livemode: true
                    enabled_events: ['*']
                    status: 'enabled'
                    description: null
                    secret: 'whsec_3f9a...'
                    created_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      summary: List webhook endpoints
      description: |
        List webhook endpoints for the authenticated platform. Only endpoints
        matching the API key's mode are returned (a test key lists sandbox
        endpoints; a live key lists live endpoints). Newest first.

        Without the `DialStack-Account` header, this lists **platform-global**
        endpoints. Include the header to list a single account's scoped
        endpoints instead.
      operationId: listWebhookEndpoints
      tags: [Webhook Endpoints]
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/DialStackAccountOptional'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of webhook endpoints
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/webhook_endpoints/{webhook_endpoint}:
    parameters:
      - name: webhook_endpoint
        in: path
        required: true
        description: The webhook endpoint ID.
        schema:
          type: string
      # Must match the endpoint's scope: omit for a platform-global endpoint,
      # or supply the owning account for an account-scoped endpoint.
      - $ref: '#/components/parameters/DialStackAccountOptional'
    get:
      summary: Get a webhook endpoint
      operationId: getWebhookEndpoint
      tags: [Webhook Endpoints]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Webhook endpoint details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Update a webhook endpoint
      description: |
        Update an endpoint's URL, subscribed events, status, or description.
        An endpoint's mode (`livemode`) is fixed at creation and cannot change.
      operationId: updateWebhookEndpoint
      tags: [Webhook Endpoints]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookEndpointRequest'
      responses:
        '200':
          description: Webhook endpoint updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a webhook endpoint
      operationId: deleteWebhookEndpoint
      tags: [Webhook Endpoints]
      security:
        - BearerAuth: []
      responses:
        '204':
          description: Webhook endpoint deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # Schedules

  /v1/schedules:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Create a schedule
      description: |
        Create a new business hours schedule within the account. Schedules define
        when the business is open based on weekly time ranges and holidays.

      operationId: createSchedule
      tags: [Schedules]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateScheduleRequest'
            examples:
              business_hours:
                summary: Standard business hours
                value:
                  name: 'Business Hours'
                  timezone: 'America/New_York'
                  ranges:
                    - day: 1
                      start: '09:00'
                      end: '17:00'
                    - day: 2
                      start: '09:00'
                      end: '17:00'
                    - day: 3
                      start: '09:00'
                      end: '17:00'
                    - day: 4
                      start: '09:00'
                      end: '17:00'
                    - day: 5
                      start: '09:00'
                      end: '17:00'
                  holidays:
                    - start: '2025-12-25'
                      end: '2025-12-25'
      responses:
        '201':
          description: Schedule created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
              examples:
                success:
                  summary: Successful schedule creation
                  value:
                    id: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                    name: 'Business Hours'
                    timezone: 'America/New_York'
                    ranges:
                      - day: 1
                        start: '09:00'
                        end: '17:00'
                    holidays: []
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    get:
      summary: List schedules
      description: |
        List all schedules for the account.
        Results are returned in reverse chronological order (newest first).

      operationId: listSchedules
      tags: [Schedules]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of schedules
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Schedule'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                list:
                  summary: Schedule list response
                  value:
                    object: 'list'
                    url: '/v1/schedules'
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - id: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                        name: 'Business Hours'
                        timezone: 'America/New_York'
                        ranges:
                          - day: 1
                            start: '09:00'
                            end: '17:00'
                        holidays: []
                        created_at: '2025-10-18T10:00:00Z'
                        updated_at: '2025-10-18T10:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/schedules/{schedule_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/ScheduleId'
    get:
      summary: Get schedule details
      description: |
        Retrieve a specific schedule by ID.

      operationId: getSchedule
      tags: [Schedules]
      responses:
        '200':
          description: Schedule details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ScheduleNotFound'
    post:
      summary: Update schedule
      description: |
        Update schedule details (name, timezone, ranges, or holidays).
        Only the fields provided will be updated.

      operationId: updateSchedule
      tags: [Schedules]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateScheduleRequest'
            examples:
              update_name:
                summary: Update schedule name
                value:
                  name: 'New Business Hours'
              update_ranges:
                summary: Update time ranges
                value:
                  ranges:
                    - day: 1
                      start: '08:00'
                      end: '18:00'
      responses:
        '200':
          description: Schedule updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ScheduleNotFound'
    delete:
      summary: Delete schedule
      description: |
        Delete a schedule. This operation is irreversible.

      operationId: deleteSchedule
      tags: [Schedules]
      responses:
        '204':
          $ref: '#/components/responses/ScheduleDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ScheduleNotFound'

  /v1/schedules/{schedule_id}/hold:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/ScheduleId'
    post:
      summary: Set schedule hold
      description: |
        Set a temporary hold on a schedule. The hold overrides the normal schedule
        calculation until the specified time.

        Use `value: true` to force the schedule to be considered "open" regardless
        of the time ranges, or `value: false` to force it to be considered "closed".

        If `until` is not provided, it defaults to midnight (00:00:00) the next day
        in the schedule's timezone.

      operationId: setScheduleHold
      tags: [Schedules]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HoldScheduleRequest'
            examples:
              close_until_specific_time:
                summary: Close until specific time
                value:
                  value: false
                  until: '2025-12-20T22:00:00Z'
              close_until_tomorrow:
                summary: Close until midnight (default)
                value:
                  value: false
              force_open:
                summary: Force open temporarily
                value:
                  value: true
                  until: '2025-12-20T17:00:00Z'
      responses:
        '200':
          description: Hold set successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
              examples:
                with_hold:
                  summary: Schedule with hold set
                  value:
                    id: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                    name: 'Business Hours'
                    timezone: 'America/New_York'
                    ranges:
                      - day: 1
                        start: '09:00'
                        end: '17:00'
                    holidays: []
                    hold:
                      value: false
                      until: '2025-12-20T22:00:00Z'
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T14:30:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ScheduleNotFound'
    delete:
      summary: Clear schedule hold
      description: |
        Clear the hold from a schedule, returning it to normal schedule-based calculation.

      operationId: clearScheduleHold
      tags: [Schedules]
      responses:
        '200':
          description: Hold cleared successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
              examples:
                hold_cleared:
                  summary: Schedule with hold cleared
                  value:
                    id: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                    name: 'Business Hours'
                    timezone: 'America/New_York'
                    ranges:
                      - day: 1
                        start: '09:00'
                        end: '17:00'
                    holidays: []
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T14:35:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ScheduleNotFound'

  # Dial Plans

  /v1/dialplans:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Create a dial plan
      description: |
        Create a new visual call routing graph for the account.
        See the `DialPlanNode` schema for the full list of node types and
        their configuration options.

      operationId: createDialPlan
      tags: [Dial Plans]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDialPlanRequest'
            examples:
              main_line:
                summary: Main line routing with schedule check
                value:
                  name: 'Main Line Routing'
                  entry_node: 'check_hours'
                  nodes:
                    - id: 'check_hours'
                      type: 'schedule'
                      config:
                        schedule: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                        open: 'reception'
                        closed: 'voicemail'
                    - id: 'reception'
                      type: 'internal_dial'
                      config:
                        target: 'user_01h2xcejqtf2nbrexx3vqjhp45'
                        timeout: 30
                        next: 'voicemail'
                    - id: 'voicemail'
                      type: 'internal_dial'
                      config:
                        target: 'user_01h2xcejqtf2nbrexx3vqjhp46'
                        timeout: 60
      responses:
        '201':
          description: Dial plan created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DialPlan'
              examples:
                success:
                  summary: Successful dial plan creation
                  value:
                    id: 'dp_01h2xcejqtf2nbrexx3vqjhp60'
                    name: 'Main Line Routing'
                    entry_node: 'check_hours'
                    nodes:
                      - id: 'check_hours'
                        type: 'schedule'
                        config:
                          schedule: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                          open: 'reception'
                          closed: 'voicemail'
                      - id: 'reception'
                        type: 'internal_dial'
                        config:
                          target: 'user_01h2xcejqtf2nbrexx3vqjhp45'
                          timeout: 30
                          next: 'voicemail'
                      - id: 'voicemail'
                        type: 'internal_dial'
                        config:
                          target: 'user_01h2xcejqtf2nbrexx3vqjhp46'
                          timeout: 60
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/RoutingLoop'
    get:
      summary: List dial plans
      description: |
        List all dial plans for the account.
        Results are returned in reverse chronological order (newest first).

      operationId: listDialPlans
      tags: [Dial Plans]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: List of dial plans
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DialPlan'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/dialplans/{dial_plan_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: dial_plan_id
        in: path
        required: true
        description: The dial plan identifier
        schema:
          type: string
        example: 'dp_01h2xcejqtf2nbrexx3vqjhp60'
    get:
      summary: Get a dial plan
      description: |
        Retrieve a dial plan by ID.

      operationId: getDialPlan
      tags: [Dial Plans]
      parameters:
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: Dial plan details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DialPlan'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/DialPlanNotFound'
    post:
      summary: Update a dial plan
      description: |
        Update an existing dial plan. All fields are optional;
        only provided fields will be updated.

        When `nodes` is provided, it fully replaces all nodes (not a partial node update).

      operationId: updateDialPlan
      tags: [Dial Plans]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDialPlanRequest'
            examples:
              update_name:
                summary: Update dial plan name
                value:
                  name: 'Updated Main Line Routing'
              update_nodes:
                summary: Update nodes (full replacement)
                value:
                  entry_node: 'check_hours'
                  nodes:
                    - id: 'check_hours'
                      type: 'schedule'
                      config:
                        schedule: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                        open: 'reception'
                        closed: 'after_hours'
                    - id: 'reception'
                      type: 'internal_dial'
                      config:
                        target: 'user_01h2xcejqtf2nbrexx3vqjhp45'
                        timeout: 30
                    - id: 'after_hours'
                      type: 'internal_dial'
                      config:
                        target: 'user_01h2xcejqtf2nbrexx3vqjhp47'
                        timeout: 45
      responses:
        '200':
          description: Dial plan updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DialPlan'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/DialPlanNotFound'
    delete:
      summary: Delete a dial plan
      description: |
        Delete a dial plan by ID.

        **Note**: Deletion will fail if any extensions still reference this dial plan.
        You must first update or delete those extensions.

      operationId: deleteDialPlan
      tags: [Dial Plans]
      responses:
        '204':
          $ref: '#/components/responses/DialPlanDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/DialPlanNotFound'
        '409':
          $ref: '#/components/responses/DialPlanConflict'

  # Ring Group Management

  /v1/ring_groups:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Create a ring group
      description: |
        Creates a new ring group for parallel dialing.

        When a call is routed to a ring group, all members ring simultaneously.
        The first member to answer wins; other ringing channels are cancelled.

      operationId: createRingGroup
      tags: [Ring Groups]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRingGroupRequest'
      responses:
        '201':
          description: Ring group created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RingGroup'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      summary: List ring groups
      description: |
        Returns a paginated list of ring groups for the account.

      operationId: listRingGroups
      tags: [Ring Groups]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: List of ring groups
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/RingGroup'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/ring_groups/{ring_group_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/RingGroupId'

    get:
      summary: Get a ring group
      description: |
        Retrieves a ring group by ID, including its members.

      operationId: getRingGroup
      tags: [Ring Groups]
      parameters:
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: Ring group details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RingGroup'
        '404':
          $ref: '#/components/responses/RingGroupNotFound'

    post:
      summary: Update a ring group
      description: |
        Updates ring group settings. All fields are optional.

      operationId: updateRingGroup
      tags: [Ring Groups]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateRingGroupRequest'
      responses:
        '200':
          description: Updated ring group
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RingGroup'
        '404':
          $ref: '#/components/responses/RingGroupNotFound'

    delete:
      summary: Delete a ring group
      description: |
        Deletes a ring group. Cannot delete if used by an extension.

      operationId: deleteRingGroup
      tags: [Ring Groups]
      responses:
        '204':
          $ref: '#/components/responses/RingGroupDeleted'
        '404':
          $ref: '#/components/responses/RingGroupNotFound'
        '409':
          $ref: '#/components/responses/RingGroupConflict'

  /v1/ring_groups/{ring_group_id}/members:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/RingGroupId'

    post:
      summary: Add a member
      description: |
        Adds a member to the ring group.

        Exactly one of `extension` or `phone_number` must be provided:
        - `extension`: ID of a routing target
        - `phone_number`: Dial string (E.164, local number, extension, 911, etc.)

      operationId: addRingGroupMember
      tags: [Ring Groups]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddRingGroupMemberRequest'
            examples:
              extension:
                summary: Extension member
                value:
                  extension: 'user_01h2xcejqtf2nbrexx3vqjhp42'
              phone:
                summary: Phone number member
                value:
                  phone_number: '+14155551234'
      responses:
        '201':
          description: Member added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RingGroupMember'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/RingGroupNotFound'
        '422':
          description: |
            Routing loop detected. Adding this member would create a circular reference
            (e.g., RG1 → DP1 → RG1), or nesting exceeds maximum depth of 20.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'routing loop detected: rg_01h2x... → dp_01h2x... → rg_01h2x...'

  /v1/ring_groups/{ring_group_id}/members/{member_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/RingGroupId'
      - $ref: '#/components/parameters/RingGroupMemberId'

    delete:
      summary: Remove a member
      description: |
        Removes a member from the ring group.

      operationId: removeRingGroupMember
      tags: [Ring Groups]
      responses:
        '204':
          $ref: '#/components/responses/RingGroupMemberDeleted'
        '404':
          $ref: '#/components/responses/RingGroupMemberNotFound'

  # Shared Voicemail Box Management

  # Files

  /v1/files:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Upload a file
      description: |
        Uploads a file and returns its metadata, including the `id` to
        reference elsewhere. The `purpose` determines the accepted content and
        validation. `fax_source` (for sending a fax) accepts a single PDF up to
        25 MB and 50 pages.

        Uploads are served from a dedicated host: send this request to
        `https://files.dialstack.ai/v1/files` (not `api.dialstack.ai`, which
        returns a 404 for this operation). Authentication and the request body
        are otherwise identical. All other API operations use the regular
        `api.dialstack.ai` host.
      operationId: uploadFile
      servers:
        - url: https://files.dialstack.ai
      tags: [Files]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: The file to upload.
                purpose:
                  type: string
                  description: What the file will be used for.
                  example: fax_source
              required:
                - file
                - purpose
      responses:
        '201':
          description: The file was uploaded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/File'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: The uploaded file exceeds the size limit for this purpose.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # Fax Management

  /v1/faxes:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    get:
      summary: List faxes
      description: |
        Returns a paginated list of faxes for the account, most recent first.
      operationId: listFaxes
      tags: [Faxes]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: direction
          in: query
          description: Filter by fax direction.
          schema:
            type: string
            enum: [inbound, outbound]
        - name: status
          in: query
          description: Filter by fax status.
          schema:
            type: string
            enum: [pending, delivered, failed, received]
        - name: did
          in: query
          description: Filter by your own associated phone number (DID), by its id.
          schema:
            type: string
        - name: number
          in: query
          description: |
            Substring filter on the sender or recipient number. Non-digit
            characters are stripped, then the digits are matched anywhere within
            the stored E.164 value — e.g. `415` returns every fax to or from a
            415-area-code number.
          schema:
            type: string
          example: '4155551234'
        - name: is_read
          in: query
          description: Filter by read state. Pass `false` to show only unread faxes.
          schema:
            type: boolean
        - name: 'expand[]'
          in: query
          description: 'Related resources to inline. Supported values: `file`, `did`.'
          schema:
            type: array
            items:
              type: string
              enum: [file, did]
      responses:
        '200':
          description: List of faxes
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Fax'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                default:
                  summary: A page of faxes (one outbound, one inbound)
                  value:
                    object: list
                    url: /v1/faxes
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - id: fax_01h2xcejqtf2nbrexx3vqjhp60
                        direction: outbound
                        status: delivered
                        file: file_01h2xcejqtf2nbrexx3vqjhp99
                        did: did_01h2xcejqtf2nbrexx3vqjhp33
                        from_number: '+14155550100'
                        to_number: '+12125558888'
                        call_id: null
                        pages: 3
                        source_pages: 3
                        transport: t38
                        error_code: null
                        attempts: 1
                        read_at: null
                        submitted_at: '2026-06-10T18:04:00Z'
                        completed_at: '2026-06-10T18:05:12Z'
                        created_at: '2026-06-10T18:04:00Z'
                        updated_at: '2026-06-10T18:05:12Z'
                      - id: fax_01h2xcejqtf2nbrexx3vqjhp61
                        direction: inbound
                        status: received
                        file: file_01h2xcejqtf2nbrexx3vqjhpa1
                        did: did_01h2xcejqtf2nbrexx3vqjhp33
                        from_number: '+13055554444'
                        to_number: '+14155550100'
                        call_id: null
                        pages: 5
                        source_pages: null
                        transport: t38
                        error_code: null
                        attempts: 0
                        read_at: null
                        submitted_at: null
                        completed_at: '2026-06-10T17:40:09Z'
                        created_at: '2026-06-10T17:39:30Z'
                        updated_at: '2026-06-10T17:40:09Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      summary: Send a fax
      description: |
        Sends an outbound fax. Upload the document first with
        `POST https://files.dialstack.ai/v1/files` (purpose `fax_source`) —
        note the dedicated upload host — to obtain a `file`, then
        reference it here along with the source number (`did`) and the
        destination. The source number must be active and fax-enabled. The
        destination is normalized to E.164.

        The fax is created in `pending` and progresses to `delivered` or
        `failed`. Outbound sends are subject to a rolling-hour, per-account
        rate limit.
      operationId: sendFax
      tags: [Faxes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                file:
                  type: string
                  description: ID of a file uploaded with purpose `fax_source`.
                to:
                  type: string
                  description: Destination fax number. Normalized to E.164.
                  example: '+14155551234'
                did:
                  type: string
                  description: ID of the account phone number (DID) to send from. Must be active and fax-enabled.
              required:
                - file
                - to
                - did
      responses:
        '201':
          description: The fax was accepted and queued for delivery.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Fax'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          description: The account's rolling-hour fax rate limit has been exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/faxes/{fax}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/FaxId'

    delete:
      summary: Delete a fax
      description: |
        Deletes a fax record. The referenced file keeps its own lifecycle.
      operationId: deleteFax
      tags: [Faxes]
      responses:
        '204':
          $ref: '#/components/responses/FaxDeleted'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/FaxNotFound'

  /v1/shared_voicemail_boxes:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Create a shared voicemail box
      description: |
        Creates a new shared voicemail box for team or departmental voicemail.

      operationId: createSharedVoicemailBox
      tags: [Shared Voicemail Boxes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSharedVoicemailBoxRequest'
      responses:
        '201':
          description: Shared voicemail box created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SharedVoicemailBox'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      summary: List shared voicemail boxes
      description: |
        Returns a paginated list of shared voicemail boxes for the account.

      operationId: listSharedVoicemailBoxes
      tags: [Shared Voicemail Boxes]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: List of shared voicemail boxes
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SharedVoicemailBox'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/shared_voicemail_boxes/{shared_voicemail_box_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/SharedVoicemailBoxId'

    get:
      summary: Get a shared voicemail box
      description: |
        Retrieves a shared voicemail box by ID.

      operationId: getSharedVoicemailBox
      tags: [Shared Voicemail Boxes]
      parameters:
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: Shared voicemail box details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SharedVoicemailBox'
        '404':
          $ref: '#/components/responses/SharedVoicemailBoxNotFound'

    post:
      summary: Update a shared voicemail box
      description: |
        Updates shared voicemail box settings. All fields are optional.

      operationId: updateSharedVoicemailBox
      tags: [Shared Voicemail Boxes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSharedVoicemailBoxRequest'
      responses:
        '200':
          description: Updated shared voicemail box
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SharedVoicemailBox'
        '404':
          $ref: '#/components/responses/SharedVoicemailBoxNotFound'

    delete:
      summary: Delete a shared voicemail box
      description: |
        Deletes a shared voicemail box. Cannot delete if used by an extension.

      operationId: deleteSharedVoicemailBox
      tags: [Shared Voicemail Boxes]
      responses:
        '204':
          $ref: '#/components/responses/SharedVoicemailBoxDeleted'
        '404':
          $ref: '#/components/responses/SharedVoicemailBoxNotFound'
        '409':
          $ref: '#/components/responses/SharedVoicemailBoxConflict'

  # Audio Clip Management

  /v1/audio_clips:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Upload an audio clip
      description: |
        Uploads a new audio clip for use as hold music, IVR prompts, or other audio playback.

        Accepts a multipart form with a `file` field (audio file) and a `name` field (display name).
        The audio is validated and transcoded server-side. Maximum file size: 5 MB.
        Supported input formats: WAV, MP3, AAC, Ogg Vorbis, Opus.

      operationId: createAudioClip
      tags: [Audio Clips]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 255
                  description: Display name for the audio clip (must be unique per account)
                  example: 'Hold Music'
                file:
                  type: string
                  format: binary
                  description: Audio file to upload
              required:
                - name
                - file
      responses:
        '201':
          description: Audio clip created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudioClip'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: An audio clip with this name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: File size exceeds the 5 MB limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      summary: List audio clips
      description: |
        Returns a paginated list of audio clips for the account.

      operationId: listAudioClips
      tags: [Audio Clips]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of audio clips
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AudioClip'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/audio_clips/{audio_clip_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/AudioClipId'

    get:
      summary: Get an audio clip
      description: |
        Retrieves an audio clip by ID. The response includes a signed URL for downloading the audio file.

      operationId: getAudioClip
      tags: [Audio Clips]
      responses:
        '200':
          description: Audio clip details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudioClip'
        '404':
          $ref: '#/components/responses/AudioClipNotFound'

    post:
      summary: Update an audio clip
      description: |
        Updates an audio clip's display name.

      operationId: updateAudioClip
      tags: [Audio Clips]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAudioClipRequest'
      responses:
        '200':
          description: Updated audio clip
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudioClip'
        '404':
          $ref: '#/components/responses/AudioClipNotFound'
        '409':
          description: An audio clip with this name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    delete:
      summary: Delete an audio clip
      description: |
        Deletes an audio clip.

      operationId: deleteAudioClip
      tags: [Audio Clips]
      responses:
        '204':
          $ref: '#/components/responses/AudioClipDeleted'
        '404':
          $ref: '#/components/responses/AudioClipNotFound'

  # Extension Management

  /v1/extensions:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Create an extension
      description: |
        Creates a new extension mapping a dial code to a routing target.

      operationId: createExtension
      tags: [Extensions]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateExtensionRequest'
            examples:
              user_extension:
                summary: Create a user extension
                value:
                  number: '105'
                  target: 'user_01h2xcejqtf2nbrexx3vqjhp42'
              dial_plan_extension:
                summary: Create a dial plan extension
                value:
                  number: '200'
                  target: 'dp_01h2xcejqtf2nbrexx3vqjhp60'
              voice_app_extension:
                summary: Create a voice app extension
                value:
                  number: '300'
                  target: 'va_01h2xcejqtf2nbrexx3vqjhp49'
      responses:
        '201':
          description: Extension created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Extension'
              examples:
                success:
                  summary: Successful extension creation
                  value:
                    number: '105'
                    target: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                    status: 'active'
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/ExtensionConflict'
    get:
      summary: List extensions
      description: |
        List all extensions for the account.
        Optionally filter by target to find extensions pointing to a specific routing target.
        Results are returned in reverse chronological order (newest first).

      operationId: listExtensions
      tags: [Extensions]
      parameters:
        - $ref: '#/components/parameters/ExtensionTargetFilter'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of extensions
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Extension'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                list:
                  summary: Extension list response
                  value:
                    object: 'list'
                    url: '/v1/extensions'
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - number: '105'
                        target: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                        status: 'active'
                        created_at: '2025-10-18T10:00:00Z'
                        updated_at: '2025-10-18T10:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/extensions/{number}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/ExtensionNumber'
    get:
      summary: Get an extension
      description: |
        Retrieve an extension by its number (dial code).

      operationId: getExtension
      tags: [Extensions]
      responses:
        '200':
          description: Extension details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Extension'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ExtensionNotFound'
    post:
      summary: Update an extension
      description: |
        Update an existing extension. You can change the target, the extension
        number, or both in one request (applied atomically). Renaming to a
        number already in use in the account returns `409`.

      operationId: updateExtension
      tags: [Extensions]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateExtensionRequest'
            examples:
              update_target:
                summary: Update extension target
                value:
                  target: 'user_01h2xcejqtf2nbrexx3vqjhp43'
              rename_number:
                summary: Rename the extension number
                value:
                  number: '1200'
      responses:
        '200':
          description: Extension updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Extension'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ExtensionNotFound'
        '409':
          description: The requested extension number is already in use in this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'extension number already exists'
        '422':
          description: Loop detected - updating to target a ring group would create a circular reference, or nesting exceeds maximum depth of 10
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'extension is a member of the target ring group'
    delete:
      summary: Delete an extension
      description: |
        Delete an extension by its number (dial code).

      operationId: deleteExtension
      tags: [Extensions]
      responses:
        '204':
          $ref: '#/components/responses/ExtensionDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/ExtensionNotFound'

  # Programmable Button Templates

  /v1/button_templates:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Create a button template
      description: Create a reusable programmable-key template for an account.
      operationId: createButtonTemplate
      tags: [Button Templates]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateButtonTemplateRequest'
      responses:
        '201':
          description: Button template created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ButtonTemplate'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      summary: List button templates
      description: Returns a paginated list of button templates for the account.
      operationId: listButtonTemplates
      tags: [Button Templates]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of button templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ButtonTemplate'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/button_templates/{id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/ButtonTemplateId'

    get:
      summary: Get a button template
      operationId: getButtonTemplate
      tags: [Button Templates]
      parameters:
        - name: for_device
          in: query
          required: false
          description: |
            Device identifier. Attaches a `compatibility` block showing how this
            template would render on the named device (before binding).
          schema:
            type: string
            example: 'dev_01h2xcejqtf2nbrexx3vqjhp50'
        - name: 'expand[]'
          in: query
          required: false
          description: Set to `buttons` to embed the template's buttons in the response.
          schema:
            type: array
            items:
              type: string
              enum: [buttons]
      responses:
        '200':
          description: Button template details
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ButtonTemplate'
                  - type: object
                    properties:
                      buttons:
                        type: array
                        description: Present only when `expand[]=buttons`.
                        items:
                          $ref: '#/components/schemas/TemplateButton'
                      compatibility:
                        allOf:
                          - $ref: '#/components/schemas/ButtonCompatibilitySummary'
                        description: Present only when `for_device` is supplied.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ButtonTemplateNotFound'

    post:
      summary: Update a button template
      operationId: updateButtonTemplate
      tags: [Button Templates]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateButtonTemplateRequest'
      responses:
        '200':
          description: Button template updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ButtonTemplate'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ButtonTemplateNotFound'

    delete:
      summary: Delete a button template
      description: >-
        Soft-deletes the template, clears it from every device currently bound
        to it, and sets the owning account's `default_button_template` to null
        when this template was the account default.
      operationId: deleteButtonTemplate
      tags: [Button Templates]
      responses:
        '204':
          description: Button template deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ButtonTemplateNotFound'

  /v1/button_templates/{id}/buttons:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/ButtonTemplateId'

    get:
      summary: List template buttons
      operationId: listTemplateButtons
      tags: [Button Templates]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of template buttons
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TemplateButton'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ButtonTemplateNotFound'

    post:
      summary: Add a template button
      operationId: createTemplateButton
      tags: [Button Templates]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTemplateButtonRequest'
      responses:
        '201':
          description: Template button created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateButton'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ButtonTemplateNotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /v1/button_templates/{id}/buttons/{button_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/ButtonTemplateId'
      - $ref: '#/components/parameters/ButtonId'

    post:
      summary: Update a template button
      description: |
        Moves a button to a new position. When the target position is already
        occupied, the buttons between the old and new position rotate over the
        template's occupied positions — the set of occupied positions never
        changes, so gaps in the layout are preserved. When the target position
        is free, only the moved button changes.

        `position` is the only updatable field.
      operationId: updateTemplateButton
      tags: [Button Templates]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTemplateButtonRequest'
      responses:
        '200':
          description: Template button updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateButton'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ButtonTemplateNotFound'

    delete:
      summary: Delete a template button
      operationId: deleteTemplateButton
      tags: [Button Templates]
      responses:
        '204':
          description: Template button deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ButtonTemplateNotFound'

  # Device Management (Unified read-only view of all device types)

  /v1/devices:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Create a device
      description: |
        **Recommended** way to create any device. Prefer this endpoint over
        `POST /v1/deskphones`, `POST /v1/dect-bases`, and
        `POST /v1/dect-bases/{id}/handsets`.

        Supported types:
        - `deskphone` requires `mac_address`
        - `dect_base` requires `mac_address`
        - `dect_handset` requires `ipei`. `base` is optional —
          omit to stock the handset as unpaired and pair it later.

        `type` is optional for MAC-addressable devices: omit it and the device
        type is detected from `mac_address` via the vendor product catalog. If
        the MAC cannot be classified, the request returns `400` and you must
        supply `type`. Supplying a `type` that contradicts a positive catalog
        match also returns `400` (the request is never silently reclassified).
        `dect_handset` is IPEI-identified, so its `type` must always be supplied.
      operationId: createDevice
      tags: [Devices]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDeviceRequest'
            examples:
              deskphone:
                summary: Create a deskphone
                value:
                  type: deskphone
                  mac_address: '00:04:13:aa:bb:cc'
                  model: 'D785'
              detected:
                summary: Create a device, detecting the type from the MAC
                value:
                  mac_address: '00:04:13:aa:bb:cc'
                  model: 'D785'
              dect_base:
                summary: Create a DECT base
                value:
                  type: dect_base
                  mac_address: '00:04:13:11:22:33'
                  model: 'M500'
              dect_handset:
                summary: Create a DECT handset
                value:
                  type: dect_handset
                  base: '01h2xcejqtf2nbrexx3vqjhp50'
                  ipei: '123456789012345'
                  name: 'Front Desk'
      responses:
        '201':
          description: Device created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateDeviceResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Parent DECT base not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Device creation conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      summary: List devices
      description: |
        **Recommended** way to list devices. Prefer this endpoint over
        `GET /v1/deskphones` and `GET /v1/dect-bases`.

        Unified view of devices (deskphones, DECT base stations, and DECT handsets).
        Use the optional `type` filter to return only one device type.
        Use `expand[]=users` to include device/user assignment metadata.
        Results are returned in reverse chronological order (newest first).

      operationId: listDevices
      tags: [Devices]
      parameters:
        - $ref: '#/components/parameters/DeviceTypeFilter'
        - $ref: '#/components/parameters/Limit'
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `users`.'
          schema:
            type: array
            items:
              type: string
              enum: [users]
      responses:
        '200':
          description: List of devices
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Device'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/devices/{id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Device identifier
        schema:
          type: string

    get:
      summary: Get a device
      description: |
        **Recommended** way to fetch any device. Prefer this endpoint over
        `GET /v1/deskphones/{id}`, `GET /v1/dect-bases/{id}`, and
        `GET /v1/dect-bases/{id}/handsets/{handset_id}`.

        Unified view of a single device. Works for deskphones, DECT base stations, and DECT handsets.

      operationId: getDevice
      tags: [Devices]
      responses:
        '200':
          description: Device details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Device'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

    post:
      summary: Update a device
      description: |
        Update a device by ID. Works for deskphones, DECT base stations, and DECT handsets.
        DECT handset updates support `name`, `ipei`, and `model`.

      operationId: updateDevice
      tags: [Devices]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDeviceRequest'
      responses:
        '200':
          description: Device updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Device'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'
        '409':
          description: |
            Update conflicts with current device state — for example, pairing
            a DECT handset with a base station that has no free slots.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceUserConflictResponse'

    delete:
      summary: Delete a device
      description: |
        **Recommended** way to delete any device. Prefer this endpoint over
        `DELETE /v1/deskphones/{id}`, `DELETE /v1/dect-bases/{id}`, and
        `DELETE /v1/dect-bases/{id}/handsets/{handset_id}`.

        Delete any device by its ID. Works for deskphones, DECT base stations, and DECT handsets.

        Deleting a deskphone removes all associated user assignments. Deleting a DECT base
        keeps its handsets: they become unpaired (no base station) and can be paired with
        another base station later. Their user assignments are preserved.

      operationId: deleteDevice
      tags: [Devices]
      responses:
        '204':
          description: Device deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

  /v1/devices/{id}/compatible_button_templates:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Device identifier
        schema:
          type: string

    get:
      summary: List button templates compatible with a device
      description: |
        Returns a paginated list of the account's button templates that render
        at least one supported button on this device. Standard cursor
        pagination applies.
      operationId: listCompatibleButtonTemplates
      tags: [Devices]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Compatible button templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ButtonTemplate'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

  /v1/devices/{id}/buttons:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Device identifier
        schema:
          type: string

    get:
      summary: List effective device buttons
      description: |
        Returns the compatibility-filtered button set that will be sent to
        provisioning for this device. Template buttons and per-device
        overrides are merged by position. The aggregate `compatibility`
        block (including unsupported buttons) is returned on the parent
        device retrieve (`GET /v1/devices/{id}`).
      operationId: listDeviceButtons
      tags: [Devices]
      responses:
        '200':
          description: Effective programmable buttons
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/MaterializedButton'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

  /v1/devices/{id}/button_overrides:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Device identifier
        schema:
          type: string

    get:
      summary: List device button overrides
      operationId: listDeviceButtonOverrides
      tags: [Devices]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of per-device button overrides
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DeviceButtonOverride'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

    post:
      summary: Create a device button override
      operationId: createDeviceButtonOverride
      tags: [Devices]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDeviceButtonOverrideRequest'
      responses:
        '201':
          description: Device button override created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceButtonOverride'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /v1/devices/{id}/button_overrides/{override_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Device identifier
        schema:
          type: string
      - $ref: '#/components/parameters/ButtonOverrideId'

    delete:
      summary: Delete a device button override
      operationId: deleteDeviceButtonOverride
      tags: [Devices]
      responses:
        '204':
          description: Device button override deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ButtonOverrideNotFound'

  # Deskphone Management

  /v1/deskphones:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Create a deskphone
      deprecated: true
      description: |
        **Deprecated:** Use `POST /v1/devices` with `type: deskphone` instead.

        Provision a new deskphone. The vendor is auto-detected from the MAC address.

      operationId: createDeskphone
      tags: [Deskphones]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDeskphoneRequest'
            examples:
              basic:
                summary: Create a deskphone
                value:
                  mac_address: '00:04:13:aa:bb:cc'
                  model: 'D785'
      responses:
        '201':
          description: Deskphone created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionedDevice'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: A device with this MAC address already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      summary: List deskphones
      deprecated: true
      description: |
        **Deprecated:** Use `GET /v1/devices?type=deskphone` instead.

        List all deskphones for the account.
        Results are returned in reverse chronological order (newest first).

      operationId: listDeskphones
      tags: [Deskphones]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of deskphones
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ProvisionedDevice'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/deskphones/{id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Deskphone identifier
        schema:
          type: string

    get:
      summary: Get a deskphone
      deprecated: true
      description: |
        **Deprecated:** Use `GET /v1/devices/{id}` instead.

        Retrieve a deskphone by its ID.

      operationId: getDeskphone
      tags: [Deskphones]
      responses:
        '200':
          description: Deskphone details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionedDevice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

    post:
      summary: Update a deskphone
      description: |
        Update a deskphone's properties. Only provided fields are changed.

      operationId: updateDeskphone
      tags: [Deskphones]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDeskphoneRequest'
      responses:
        '200':
          description: Deskphone updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionedDevice'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

    delete:
      summary: Delete a deskphone
      deprecated: true
      description: |
        **Deprecated:** Use `DELETE /v1/devices/{id}` instead.

        Delete a deskphone and all its associated lines.

      operationId: deleteDeskphone
      tags: [Deskphones]
      responses:
        '204':
          $ref: '#/components/responses/DeviceDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

  # Deskphone Sub-resources

  /v1/deskphones/{id}/provisioning-events:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Deskphone identifier
        schema:
          type: string

    get:
      summary: List provisioning events
      description: |
        List provisioning events for a deskphone. Returns a log of configuration
        fetches by the device.

      operationId: listDeskphoneProvisioningEvents
      tags: [Deskphones]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of provisioning events
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ProvisioningEvent'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

  /v1/devices/{id}/status/check-sync:
    post:
      summary: Trigger device configuration reload
      description: |
        Tells the device to fetch and apply its latest configuration.
        Works for deskphones, DECT bases, and DECT handsets — on a DECT
        system, one request reloads configuration on every handset paired
        with the base.

        With the default `reboot: false`, the device applies configuration
        in place. Active calls continue and the device stays registered.
        With `reboot: true`, the device reboots immediately — any active
        call is dropped, and the device is offline for about 30–90 seconds.

        Trigger check-sync when you actually change configuration, not on
        every admin-UI interaction. See the [Device provisioning
        guide](https://docs.dialstack.ai/guides/device-provisioning) for
        the full list of side effects.

      operationId: deviceCheckSync
      tags: [Devices]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Device identifier (deskphone, DECT base, or DECT handset)
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reboot:
                  type: boolean
                  description: |
                    If true, the device reboots immediately after applying
                    config, interrupting any active call. Default `false`
                    performs a non-disruptive config reload.
                  default: false
            examples:
              default:
                summary: Default (no reboot)
                value: {}
              with_reboot:
                summary: With reboot — destructive, interrupts active calls
                value:
                  reboot: true
      responses:
        '200':
          description: |
            At least one line accepted the request. Inspect `lines` for the
            per-line outcome — some lines may be `unreachable` or
            `not_registered` even when the overall request succeeds.
          content:
            application/json:
              schema:
                type: object
                required: [success, lines_notified, lines]
                properties:
                  success:
                    type: boolean
                    example: true
                  lines_notified:
                    type: integer
                    description: Number of lines that accepted the request.
                    example: 1
                  lines:
                    type: array
                    description: |
                      Per-line outcome. `line_number` is a 1-indexed
                      identifier; for deskphones it matches the physical
                      line, for DECT it is a stable sequence within the
                      response. A `line_number` of `0` with `management:
                      true` denotes the device-level reprovision attempt
                      (see `management`).
                    items:
                      type: object
                      required: [line_number, status]
                      properties:
                        line_number:
                          type: integer
                          example: 1
                        status:
                          type: string
                          enum: [delivered, not_registered, unreachable, error]
                        management:
                          type: boolean
                          description: |
                            Present and `true` when this entry is the
                            device-level reprovision attempt, used to reach
                            a device that has no assigned line yet (or has
                            not finished provisioning its line). `line_number`
                            is `0` for this entry.
                          example: true
        '400':
          description: Invalid device ID or device has no SIP lines configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_id:
                  value:
                    error: 'Invalid device ID'
                no_lines:
                  value:
                    error: 'Device has no SIP lines configured'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Device not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'Device not found'
        '409':
          description: |
            The device could not be reached. The `reason` field tells you why:

            - `not_registered` — the device has no active registration. It
              may be powered off, unable to reach the SIP server, or has
              never registered.
            - `unreachable` — the device didn't respond in time. It is
              likely offline or has lost its connection.
          content:
            application/json:
              schema:
                type: object
                required: [error, reason]
                properties:
                  error:
                    type: string
                  reason:
                    type: string
                    enum: [not_registered, unreachable]
              examples:
                not_registered:
                  value:
                    error: 'Device is not registered - phone may be offline'
                    reason: not_registered
                unreachable:
                  value:
                    error: 'Device did not respond - it may be offline'
                    reason: unreachable
        '503':
          description: Check-sync not available in this environment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'Check-sync not available in this environment'

  # Device User Assignment

  /v1/devices/{id}/users:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Device identifier (deskphone, DECT handset, or DECT base)
        schema:
          type: string

    post:
      summary: Assign a user to a device
      description: |
        Assigns a user to a device. SIP credentials and line configuration are created automatically.

        Supported device types:
        - **Deskphones** — assigns the user to the next available line key (max 24)
        - **DECT handsets** — assigns the user to the handset (max 24)
        - **DECT bases** — not supported (returns 400); assign to individual handsets instead

      operationId: assignUserToDevice
      tags: [Devices]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssignUserToDeviceRequest'
            examples:
              basic:
                summary: Assign a user to a deskphone
                value:
                  user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
      responses:
        '201':
          description: User assigned to deskphone
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceUserAssignment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'
        '409':
          description: |
            Conflict. The user is already on this device, on a different
            device (one-device-per-user), or the device is full.
            Clients should branch on `code`; `existing_device` is
            present when `code` is `user_already_has_device` and points
            at the device the user is currently bound to.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceUserConflictResponse'
              examples:
                already_assigned:
                  value:
                    error: 'user is already assigned to this device'
                    code: 'user_already_assigned'
                user_already_has_device:
                  value:
                    error: 'user is already assigned to another device'
                    code: 'user_already_has_device'
                    existing_device: 'dev_01h2xcejqtf2nbrexx3vqjhp40'
                user_already_has_endpoint:
                  value:
                    error: 'user already has an active endpoint'
                    code: 'user_already_has_endpoint'
                handset_already_assigned:
                  value:
                    error: 'handset already has an assigned user'
                    code: 'handset_already_assigned'
                max_lines:
                  value:
                    error: 'device has reached the maximum of 24 lines'
                    code: 'device_full'

    get:
      summary: List users assigned to a device
      description: |
        Returns all users currently assigned to the device.
        Devices support at most 24 users, so no pagination is needed.

      operationId: listDeviceUsers
      tags: [Devices]
      responses:
        '200':
          description: List of assigned users
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    enum: [list]
                  url:
                    type: string
                  next_page_url:
                    type: [string, 'null']
                  previous_page_url:
                    type: [string, 'null']
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DeviceUserAssignment'
                required:
                  - object
                  - url
                  - data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

  /v1/devices/{id}/users/{user_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Device identifier (deskphone, DECT handset, or DECT base)
        schema:
          type: string
      - $ref: '#/components/parameters/UserId'

    delete:
      summary: Remove a user from a device
      description: |
        Removes the user's assignment from the device.
        The associated SIP credentials and line configuration are cleaned up automatically.

      operationId: removeUserFromDevice
      tags: [Devices]
      responses:
        '204':
          description: User removed from deskphone
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Device not found or user is not assigned to this device
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # Deskphone Line Management

  /v1/deskphones/{id}/lines:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Deskphone identifier
        schema:
          type: string

    post:
      summary: Create a deskphone line
      deprecated: true
      description: |
        **Deprecated:** Use `POST /v1/devices/{id}/users` instead.

        Assigns a SIP endpoint to the next available line key on the deskphone.
        The line number is automatically assigned as the lowest unused number (1-24).
        A deskphone supports a maximum of 24 lines. Each endpoint can only be assigned once per device.

      operationId: createDeskphoneLine
      tags: [Deskphones]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDeviceLineRequest'
            examples:
              basic:
                summary: Assign an endpoint to a line
                value:
                  endpoint: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
      responses:
        '201':
          description: Device line created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceLine'
              examples:
                success:
                  summary: Successful line creation
                  value:
                    id: 'dln_01h2xcejqtf2nbrexx3vqjhp55'
                    device: 'dev_01h2xcejqtf2nbrexx3vqjhp50'
                    endpoint: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
                    line_number: 1
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'
        '409':
          description: Conflict (endpoint already assigned or maximum lines reached)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                duplicate_endpoint:
                  value:
                    error: 'Endpoint is already assigned to this device'
                max_lines:
                  value:
                    error: 'Device has reached the maximum of 24 lines'

    get:
      summary: List deskphone lines
      deprecated: true
      description: |
        **Deprecated:** Use `GET /v1/devices/{id}/users` instead.

        List all SIP line assignments for a deskphone.
        Returns lines ordered by line number.

      operationId: listDeskphoneLines
      tags: [Deskphones]
      responses:
        '200':
          description: List of device lines
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DeviceLine'
                required:
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

  /v1/deskphones/{id}/lines/{line_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: Deskphone identifier
        schema:
          type: string
      - $ref: '#/components/parameters/DeviceLineId'

    post:
      summary: Update a deskphone line
      deprecated: true
      description: |
        **Deprecated:** Use `DELETE /v1/devices/{id}/users/{user_id}` and `POST /v1/devices/{id}/users` instead.

        Reassigns a deskphone line to a different SIP endpoint.
        The line number remains unchanged; only the endpoint is updated.

      operationId: updateDeskphoneLine
      tags: [Deskphones]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDeviceLineRequest'
            examples:
              reassign:
                summary: Reassign to a different endpoint
                value:
                  endpoint: 'ep_01h2xcejqtf2nbrexx3vqjhp44'
      responses:
        '200':
          description: Device line updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceLine'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceLineNotFound'

    delete:
      summary: Delete a deskphone line
      deprecated: true
      description: |
        **Deprecated:** Use `DELETE /v1/devices/{id}/users/{user_id}` instead.

        Removes a SIP line assignment from the deskphone.

      operationId: deleteDeskphoneLine
      tags: [Deskphones]
      responses:
        '204':
          $ref: '#/components/responses/DeviceLineDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceLineNotFound'

  # DECT Base Management

  /v1/dect-bases:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Create a DECT base station
      deprecated: true
      description: |
        **Deprecated:** Use `POST /v1/devices` with `type: dect_base` instead.

        Register a new DECT base station for provisioning.
        The vendor is automatically detected from the MAC address.

      operationId: createDECTBase
      tags: [DECT Bases]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDECTBaseRequest'
            examples:
              standalone:
                summary: Standalone base station
                value:
                  mac_address: '00:04:13:bb:cc:dd'
              multicell_secondary:
                summary: Secondary base in multicell deployment
                value:
                  mac_address: '00:04:13:bb:cc:ee'
                  multicell_role: secondary
      responses:
        '201':
          description: DECT base created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DECTBase'
              examples:
                success:
                  summary: Successful creation
                  value:
                    id: 'dectb_01h2xcejqtf2nbrexx3vqjhp60'
                    mac_address: '00:04:13:bb:cc:dd'
                    vendor: 'snom'
                    model: null
                    status: 'pending-sync'
                    multicell_role: 'single'
                    max_handsets: 30
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: A device with this MAC address already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'A device with this MAC address already exists'

    get:
      summary: List DECT base stations
      deprecated: true
      description: |
        **Deprecated:** Use `GET /v1/devices?type=dect_base` instead.

        List all DECT base stations in the account.

      operationId: listDECTBases
      tags: [DECT Bases]
      responses:
        '200':
          description: List of DECT base stations
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DECTBase'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/dect-bases/{id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: DECT base station identifier
        schema:
          type: string

    get:
      summary: Get a DECT base station
      deprecated: true
      description: |
        **Deprecated:** Use `GET /v1/devices/{id}` instead.

        Retrieve a DECT base station by ID.

      operationId: getDECTBase
      tags: [DECT Bases]
      responses:
        '200':
          description: DECT base station details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DECTBase'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: DECT base not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'DECT base not found'

    post:
      summary: Update a DECT base station
      description: |
        Update properties of a DECT base station.

      operationId: updateDECTBase
      tags: [DECT Bases]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDECTBaseRequest'
            examples:
              update_role:
                summary: Change multicell role
                value:
                  multicell_role: data_master
              update_overrides:
                summary: Set device overrides
                value:
                  overrides:
                    abstractions:
                      regional:
                        timezone: 'America/Toronto'
                        language: 'fr-CA'
      responses:
        '200':
          description: DECT base updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DECTBase'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: DECT base not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'DECT base not found'

    delete:
      summary: Delete a DECT base station
      deprecated: true
      description: |
        **Deprecated:** Use `DELETE /v1/devices/{id}` instead.

        Delete a DECT base station and all associated handsets and extensions.

      operationId: deleteDECTBase
      tags: [DECT Bases]
      responses:
        '204':
          description: DECT base deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: DECT base not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'DECT base not found'

  # DECT Handset Management

  /v1/dect-bases/{id}/handsets:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: DECT base station identifier
        schema:
          type: string

    post:
      summary: Add a handset to a DECT base
      deprecated: true
      description: |
        **Deprecated:** Use `POST /v1/devices` with `type: dect_handset` and
        `base` set to the DECT base identifier instead.

        Pair a new wireless handset with a DECT base station.
        The slot number is automatically assigned as the lowest available slot.
        IPEI (International Portable Equipment Identity) is a unique hardware identifier
        for wireless handsets, formatted as a hexadecimal string up to 20 characters.

      operationId: createDECTHandset
      tags: [DECT Bases]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateHandsetRequest'
            examples:
              basic:
                summary: Add a handset
                value:
                  ipei: '0328A0000F'
                  display_name: 'Front Desk'
      responses:
        '201':
          description: Handset created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DECTHandset'
              examples:
                success:
                  summary: Handset paired with base
                  value:
                    id: 'decth_01h2xcejqtf2nbrexx3vqjhp70'
                    base: 'dectb_01h2xcejqtf2nbrexx3vqjhp60'
                    ipei: '0328A0000F'
                    status: 'pending-sync'
                    display_name: 'Front Desk'
                    slot_number: 1
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: DECT base not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'DECT base not found'
        '409':
          description: Handset limit reached or IPEI already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                max_handsets:
                  value:
                    error: 'Base station has reached the maximum number of handsets'
                duplicate_ipei:
                  value:
                    error: 'A handset with this IPEI already exists on this base'

    get:
      summary: List handsets on a DECT base
      description: |
        List all wireless handsets paired with a DECT base station.

      operationId: listDECTHandsets
      tags: [DECT Bases]
      responses:
        '200':
          description: List of handsets
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DECTHandset'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/dect-bases/{id}/handsets/{handset_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: DECT base station identifier
        schema:
          type: string
      - name: handset_id
        in: path
        required: true
        description: Handset identifier
        schema:
          type: string

    get:
      summary: Get a handset
      deprecated: true
      description: |
        **Deprecated:** Use `GET /v1/devices/{id}` with the handset ID instead.

        Retrieve a specific handset.

      operationId: getDECTHandset
      tags: [DECT Bases]
      responses:
        '200':
          description: Handset details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DECTHandset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Handset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'Handset not found'

    post:
      summary: Update a handset
      description: |
        Update handset properties such as IPEI, display name, or model.

        Handsets never provision, so the `model` can't be auto-detected — set it
        here to unlock programmable-button (template) support.

      operationId: updateDECTHandset
      tags: [DECT Bases]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateHandsetRequest'
            examples:
              update_name:
                summary: Update display name
                value:
                  display_name: 'Warehouse'
              update_model:
                summary: Set the handset model
                value:
                  model: 'M55'
      responses:
        '200':
          description: Handset updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DECTHandset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Handset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'Handset not found'

    delete:
      summary: Delete a handset
      deprecated: true
      description: |
        **Deprecated:** Use `DELETE /v1/devices/{id}` with the handset ID instead.

        Remove a handset from a DECT base station.
        All extensions on the handset are also removed.

      operationId: deleteDECTHandset
      tags: [DECT Bases]
      responses:
        '204':
          description: Handset deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Handset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'Handset not found'

  # DECT Extension Management

  /v1/dect-bases/{id}/handsets/{handset_id}/extensions:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: DECT base station identifier
        schema:
          type: string
      - name: handset_id
        in: path
        required: true
        description: Handset identifier
        schema:
          type: string

    post:
      summary: Assign an extension to a handset
      deprecated: true
      description: |
        **Deprecated:** Use `POST /v1/devices/{id}/users` with the handset ID instead.

        Assign a SIP endpoint to a DECT handset as an extension (SIP line).

      operationId: createDECTExtension
      tags: [DECT Bases]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateHandsetLineRequest'
            examples:
              basic:
                summary: Assign a SIP endpoint
                value:
                  endpoint: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
      responses:
        '201':
          description: Extension created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HandsetLine'
              examples:
                success:
                  summary: Extension assigned
                  value:
                    id: 'decte_01h2xcejqtf2nbrexx3vqjhp80'
                    handset: 'decth_01h2xcejqtf2nbrexx3vqjhp70'
                    endpoint: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
                    display_name: null
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Handset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'Handset not found'
        '409':
          description: Endpoint already assigned to this handset
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'Endpoint is already assigned to this handset'

    get:
      summary: List extensions on a handset
      deprecated: true
      description: |
        **Deprecated:** Use `GET /v1/devices/{id}/users` with the handset ID instead.

        List all SIP line assignments on a DECT handset.

      operationId: listDECTExtensions
      tags: [DECT Bases]
      responses:
        '200':
          description: List of extensions
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/HandsetLine'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Handset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'Handset not found'

  /v1/dect-bases/{id}/handsets/{handset_id}/extensions/{extension_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - name: id
        in: path
        required: true
        description: DECT base station identifier
        schema:
          type: string
      - name: handset_id
        in: path
        required: true
        description: Handset identifier
        schema:
          type: string
      - name: extension_id
        in: path
        required: true
        description: Extension identifier
        schema:
          type: string

    delete:
      summary: Remove an extension from a handset
      deprecated: true
      description: |
        **Deprecated:** Use `DELETE /v1/devices/{id}/users/{user_id}` with the handset ID instead.

        Remove a SIP line assignment from a DECT handset.

      operationId: deleteDECTExtension
      tags: [DECT Bases]
      responses:
        '204':
          description: Extension removed
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Extension not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'Extension not found'

  # Location Management

  /v1/locations:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    post:
      summary: Create a location
      description: |
        Create a new business location with a validated address.

      operationId: createLocation
      tags: [Locations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateLocationRequest'
            examples:
              basic:
                summary: Create main office
                value:
                  name: 'Main Office'
                  address:
                    street: '123 Main St'
                    city: 'New York'
                    state: 'NY'
                    postal_code: '10001'
                    country: 'US'
      responses:
        '201':
          description: Location created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Location'
              examples:
                success:
                  summary: Successful location creation
                  value:
                    id: 'loc_01h2xcejqtf2nbrexx3vqjhp50'
                    name: 'Main Office'
                    address:
                      city: 'New York'
                      state: 'NY'
                      postal_code: '10001'
                      country: 'US'
                      formatted_address: '123 Main St, New York, NY 10001, US'
                    status: 'active'
                    e911_status: 'none'
                    created_at: '2025-10-18T10:00:00Z'
                    updated_at: '2025-10-18T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
    get:
      summary: List locations
      description: |
        List all locations for the account.
        Results are returned in reverse chronological order (newest first).

      operationId: listLocations
      tags: [Locations]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/LocationStatus'
      responses:
        '200':
          description: List of locations
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Location'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                list:
                  summary: Location list response
                  value:
                    object: 'list'
                    url: '/v1/locations'
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - id: 'loc_01h2xcejqtf2nbrexx3vqjhp50'
                        name: 'Main Office'
                        address:
                          city: 'New York'
                          state: 'NY'
                          postal_code: '10001'
                          country: 'US'
                          formatted_address: '123 Main St, New York, NY 10001, US'
                        status: 'active'
                        e911_status: 'none'
                        created_at: '2025-10-18T10:00:00Z'
                        updated_at: '2025-10-18T10:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/locations/{location_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/LocationId'
    get:
      summary: Get location details
      description: |
        Retrieve a specific location by ID.

      operationId: getLocation
      tags: [Locations]
      responses:
        '200':
          description: Location details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Location'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/LocationNotFound'
    post:
      summary: Update location
      description: |
        Update location details. Changing the address revalidates it and
        updates the E911 registration for the location.

      operationId: updateLocation
      tags: [Locations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateLocationRequest'
      responses:
        '200':
          description: Location updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Location'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/LocationNotFound'
        '409':
          $ref: '#/components/responses/Conflict'
    delete:
      summary: Delete location
      description: |
        Delete a location and its associated address.
        This operation is irreversible.

      operationId: deleteLocation
      tags: [Locations]
      responses:
        '204':
          $ref: '#/components/responses/LocationDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/LocationNotFound'
        '409':
          description: |
            The location is the account's main location and cannot be
            deleted directly. Switch the account's `main_location` to
            another location, then retry the delete.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/locations/{location_id}/validate-e911:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/LocationId'
    post:
      summary: Validate E911 address
      description: |
        Pre-check the location's address for E911 provisioning.
        Returns the address fields that will be used during provisioning. Full address validation
        occurs during the provision-e911 call.

      operationId: validateLocationE911
      tags: [Locations]
      responses:
        '200':
          description: Address validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/E911ValidationResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/LocationNotFound'
        '502':
          description: Upstream provider address validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/locations/{location_id}/provision-e911:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/LocationId'
    post:
      summary: Provision E911
      description: |
        Start E911 provisioning for a location.
        The location must have a valid, validated address. A phone number
        assignment is not required.
        Provisioning is asynchronous; the `e911_status` field tracks progress.

      operationId: provisionLocationE911
      tags: [Locations]
      responses:
        '200':
          description: E911 provisioning started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Location'
        '400':
          description: >-
            Invalid request: the emergency address could not be validated by the
            carrier.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/LocationNotFound'
        '502':
          description: Upstream provider order failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/quality/summary:
    get:
      summary: Get quality metrics summary
      description: |
        Returns aggregated call quality metrics for the account, including
        KPI averages (MOS, jitter, packet loss) and a time-bucketed series.
        MOS is computed using the ITU-T G.107 E-model.
      operationId: getQualitySummary
      tags: [Quality]
      parameters:
        - name: from
          in: query
          required: true
          description: Start of time range (RFC 3339)
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: true
          description: End of time range (RFC 3339)
          schema:
            type: string
            format: date-time
        - name: granularity
          in: query
          required: true
          description: Time bucket granularity
          schema:
            type: string
            enum: [hourly, daily]
      responses:
        '200':
          description: Quality summary with KPIs and time series
          content:
            application/json:
              schema:
                type: object
                properties:
                  kpi:
                    type: object
                    properties:
                      avg_mos:
                        type: ['number', 'null']
                        description: Average MOS score (1.0–5.0)
                      avg_jitter_ms:
                        type: ['number', 'null']
                        description: Average jitter in milliseconds
                      avg_packet_loss_pct:
                        type: ['number', 'null']
                        description: Average packet loss percentage
                      total_calls:
                        type: integer
                        description: Total number of answered calls with quality data
                  time_series:
                    type: array
                    items:
                      type: object
                      properties:
                        bucket:
                          type: string
                          format: date-time
                        avg_mos:
                          type: ['number', 'null']
                        call_count:
                          type: integer
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/quality/worst-calls:
    get:
      summary: Get worst quality calls
      description: |
        Returns the calls with the lowest MOS scores in the given time range,
        sorted by MOS ascending (worst first). Limited to 90-day range.
      operationId: getQualityWorstCalls
      tags: [Quality]
      parameters:
        - name: from
          in: query
          required: true
          description: Start of time range (RFC 3339)
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: true
          description: End of time range (RFC 3339)
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          description: Maximum number of calls to return (default 10, max 50)
          schema:
            type: integer
            default: 10
            maximum: 50
      responses:
        '200':
          description: List of worst quality calls
          content:
            application/json:
              schema:
                type: object
                properties:
                  calls:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Call record ID
                        started_at:
                          type: string
                          format: date-time
                        direction:
                          type: string
                          enum: [inbound, outbound, internal]
                        from_number:
                          type: string
                        to_number:
                          type: string
                        duration_seconds:
                          type: integer
                        mos:
                          type: number
                          description: MOS score (1.0–5.0)
                        jitter_ms:
                          type: number
                        packet_loss_pct:
                          type: number
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/quality/device-registrations:
    get:
      summary: Get device registration status
      description: |
        Returns the registration status of all endpoints in the account,
        including whether each device is currently online or offline and
        its public IP address when available.
      operationId: getDeviceRegistrations
      tags: [Quality]
      responses:
        '200':
          description: List of device registration statuses
          content:
            application/json:
              schema:
                type: object
                properties:
                  devices:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Endpoint ID
                        name:
                          type: ['string', 'null']
                          description: Endpoint display name
                        status:
                          type: string
                          enum: [online, offline]
                          description: Current registration status
                        public_ip:
                          type: string
                          description: Public IP address of the device (when online)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /v1/catalog:
    get:
      summary: List hardware catalog
      description: |
        Returns the list of active hardware items available for selection during onboarding.
        This endpoint does not require account context.
      operationId: listCatalog
      tags: [Catalog]
      responses:
        '200':
          description: List of active hardware catalog items
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  url:
                    type: string
                    example: /v1/catalog
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/HardwareCatalogItem'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hardware-orders:
    post:
      summary: Create hardware order
      description: |
        Creates a new hardware order with the given items and submits it for
        review. Each item references a hardware catalog entry with a quantity.

        An order may contain at most 10 distinct line items. Requests that
        exceed this limit are rejected with `400`.
      operationId: createHardwareOrder
      tags: [Hardware Orders]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HardwareOrderRequest'
      responses:
        '201':
          description: Hardware order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HardwareOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      summary: List hardware orders
      description: |
        Returns all hardware orders for the account, including embedded items.
      operationId: listHardwareOrders
      tags: [Hardware Orders]
      parameters:
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `items.device`.'
          schema:
            type: array
            items:
              type: string
              enum: [items.device]
      responses:
        '200':
          description: List of hardware orders
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  url:
                    type: string
                    example: /v1/hardware-orders
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/HardwareOrder'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hardware-orders/{order_id}:
    get:
      summary: Get hardware order
      description: |
        Returns a hardware order by ID, including embedded items.
      operationId: getHardwareOrder
      tags: [Hardware Orders]
      parameters:
        - name: order_id
          in: path
          required: true
          schema:
            type: string
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `items.device`.'
          schema:
            type: array
            items:
              type: string
              enum: [items.device]
      responses:
        '200':
          description: Hardware order details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HardwareOrder'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/HardwareOrderNotFound'
    post:
      summary: Update hardware order items
      description: |
        Replaces all items on a draft order. The order must be in draft
        status; orders are currently submitted as soon as they are created,
        so this returns a conflict until a draft flow exists. To change a
        submitted order, contact DialStack — items can be adjusted during
        review.

        Sends the complete desired state of items — existing items are
        replaced atomically.

        An order may contain at most 10 distinct line items. Requests that
        exceed this limit are rejected with `400`.
      operationId: updateHardwareOrder
      tags: [Hardware Orders]
      parameters:
        - name: order_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HardwareOrderRequest'
      responses:
        '200':
          description: Hardware order updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HardwareOrder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/HardwareOrderNotFound'
        '409':
          description: Order is not in draft status

  /v1/hardware-orders/{order_id}/items/{item_id}:
    post:
      summary: Update pre-staging intents on a hardware order item
      description: |
        Updates the pre-staging intents on a single unit of an order — the
        user and/or physical location the unit is destined for once it ships.
        Fields are tri-state: an omitted field is unchanged, `null` clears
        the intent, and a value sets it. At least one field must be provided.

        Intents are applied to the real device record when the unit is
        fulfilled. Updates are allowed while the order is in `draft`,
        `submitted`, or `approved` status. `user` only applies to hardware
        types that support user assignment (deskphones and DECT handsets —
        not base stations); `location` applies to deskphones and DECT
        bases — handsets inherit location from their paired base.

        A user can only be pre-assigned if they're available — they must not
        already have a device or be pre-assigned to another ordered unit
        (one pending device per user, mirroring the assignment rule on real
        devices). Otherwise the request is rejected with `409`.
      operationId: assignHardwareOrderItem
      tags: [Hardware Orders]
      parameters:
        - name: order_id
          in: path
          required: true
          schema:
            type: string
        - name: item_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                user:
                  type: ['string', 'null']
                  description: User to assign the unit to; null clears the assignment
                location:
                  type: ['string', 'null']
                  description: >-
                    Location to stage the unit for; null clears it. Not valid
                    on DECT handsets — they inherit location from their base.
                base_item:
                  type: ['string', 'null']
                  description: >-
                    Ordered base unit to pair this handset with; null clears
                    it. Only valid on DECT handsets, and must reference a base
                    ordered on the same order. Auto-set when the order has a
                    single base.
            example:
              user: user_01h455vb4pex5vsknk084sn02t
              location: loc_01h455vb4pex5vsknk084sn02t
      responses:
        '200':
          description: The updated hardware order item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HardwareOrderItem'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Order or item not found
        '409':
          description: >-
            Conflict — the order can no longer be modified (fulfilled or
            terminal), or the user already has a device / is already
            pre-assigned to another ordered unit.

  # --- WebRTC ---

  /v1/webrtc/ice-servers:
    get:
      summary: Get ICE server credentials
      description: |
        Returns STUN and TURN server configurations for establishing WebRTC peer
        connections. TURN credentials are time-limited — fetch fresh credentials
        before the `expires_at` timestamp.

        Call this before initiating or answering a call. The returned configuration
        can be passed directly to `RTCPeerConnection` as `iceServers`.
      operationId: getIceServers
      tags: [WebRTC]
      security:
        - UserAuth: []
      responses:
        '200':
          description: ICE server configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IceServersResponse'
              example:
                ice_servers:
                  - urls: 'stun:stun.dialstack.ai:3478'
                  - urls:
                      - 'turn:turn.dialstack.ai:443?transport=tcp'
                      - 'turn:turn.dialstack.ai:443?transport=udp'
                    username: '1712793600:user_01h2xcejqtf2nbrexx3vqjhp42'
                    credential: 'aGVsbG8gd29ybGQ='
                expires_at: '2026-04-10T22:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/webrtc/push-key:
    get:
      summary: Get the Web Push application server key
      description: |
        Returns the public key the browser needs to subscribe to push
        notifications for background call wake-up. The WebRTC SDK calls this
        automatically on connect and passes the value to the browser as the
        `applicationServerKey`; you do not normally call it directly.

        The key is stable — it is safe to cache for the lifetime of a session.

        Returns `503` when push wake-up is not available in this environment. The
        softphone works normally without it; calls that cannot wake a tab fall
        through to voicemail.
      operationId: getWebPushKey
      tags: [WebRTC]
      security:
        - UserAuth: []
      responses:
        '200':
          description: The Web Push application server key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebPushKeyResponse'
              example:
                vapid_public_key: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Push wake-up is not available in this environment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/webrtc/push-subscriptions:
    post:
      summary: Register a browser push subscription
      description: |
        Stores the browser Web Push subscription so DialStack can wake a
        backgrounded or discarded softphone tab when a call arrives. The WebRTC
        SDK calls this automatically on connect; you do not normally call it
        directly.

        The subscription is keyed by its push destination URL: registering a URL
        that is already stored (for this user or another) reassigns it to the
        current user, so a shared browser only ever has one active owner.

        The subscription deliberately outlives the connection — it is what wakes
        the tab after the session is gone — so it is not removed on disconnect.
      operationId: createPushSubscription
      tags: [WebRTC]
      security:
        - UserAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePushSubscriptionRequest'
      responses:
        '201':
          description: The stored push subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushSubscription'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # --- User-Scoped Endpoints ---

  /v1/me:
    get:
      summary: Get current user profile
      description: |
        **Coming soon.** This endpoint is documented ahead of release so you can review
        the design before it ships — requests to it currently fail. If the proposed
        shape does or does not fit your use case, we would love to hear about it at
        [api@dialstack.ai](mailto:api@dialstack.ai).

        Returns the authenticated user's profile, including their assigned extensions
        and registered devices.
      operationId: getCurrentUser
      tags: [User Profile]
      security:
        - UserAuth: []
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/me/calls:
    get:
      summary: List own call history
      description: |
        **Coming soon.** This endpoint is documented ahead of release so you can review
        the design before it ships — requests to it currently fail. If the proposed
        shape does or does not fit your use case, we would love to hear about it at
        [api@dialstack.ai](mailto:api@dialstack.ai).

        Returns call records where the authenticated user was a participant.
        Results are returned in reverse chronological order (newest first).
      operationId: listOwnCalls
      tags: [User Profile]
      security:
        - UserAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: direction
          in: query
          description: Filter by call direction
          schema:
            type: string
            enum: [inbound, outbound]
        - name: from_date
          in: query
          description: Return calls on or after this date (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: to_date
          in: query
          description: Return calls before this date (ISO 8601)
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: List of call records
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CallLog'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/me/presence:
    get:
      summary: Get own presence status
      description: |
        **Coming soon.** This endpoint is documented ahead of release so you can review
        the design before it ships — requests to it currently fail. If the proposed
        shape does or does not fit your use case, we would love to hear about it at
        [api@dialstack.ai](mailto:api@dialstack.ai). To read a user's
        presence today, use `GET /v1/users/{user_id}/presence` with a platform
        API key.

        Returns the authenticated user's current presence status.
      operationId: getOwnPresence
      tags: [User Profile]
      security:
        - UserAuth: []
      responses:
        '200':
          description: Presence status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresenceStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
    put:
      summary: Update own presence status
      description: |
        **Coming soon.** This endpoint is documented ahead of release so you can review
        the design before it ships — requests to it currently fail. If the proposed
        shape does or does not fit your use case, we would love to hear about it at
        [api@dialstack.ai](mailto:api@dialstack.ai). There is no
        user-settable presence status today.

        Updates the authenticated user's presence status. Only `available`, `dnd`,
        and `away` can be set manually. The `on_call` and `offline` statuses are
        managed automatically by the system.
      operationId: updateOwnPresence
      tags: [User Profile]
      security:
        - UserAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PresenceUpdateRequest'
      responses:
        '200':
          description: Presence updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresenceStatus'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/me/directory:
    get:
      summary: List the account directory
      description: |
        Returns the colleagues the authenticated user may see — the discovery
        read that lets a softphone resolve which users to show in a contact list,
        watch with `phone.subscribePresence(userIds[])`, or offer as dial /
        transfer targets, using only its user-session token, with no platform API
        key and no account roster supplied by your backend.

        Unlike `GET /v1/users`, this returns only `user` and `display_name` —
        never full user objects, emails, or device configuration — so it is safe
        to expose to an end-user credential.

        The account is derived from the session token; you pass no ids and no
        account context. Today the directory is every other active user in your
        account. It is cursor-paginated like the other list endpoints; follow
        `next_page_url` to read the whole account. Note that presence
        subscription itself accepts at most 100 users at once, so a client
        watching a very large directory subscribes to a subset.
      operationId: listDirectory
      tags: [User Profile]
      security:
        - UserAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: The caller's account directory
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DirectoryEntry'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/me/emergency-addresses:
    post:
      summary: Register an emergency address
      description: |
        Validates a civic address against the official 911 address database
        and stores it for the authenticated user. The new address is created
        without a network binding (`registered_ip` is null); the binding is
        established later, when a softphone connects to the WebRTC SDK using
        this address — at that point it is tied to the network the device is
        connecting from.

        A user may keep several saved addresses (location profiles); the SDK
        selects the one that matches where the device currently is.
      operationId: createEmergencyAddress
      tags: [User Profile]
      security:
        - UserAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmergencyAddressRequest'
      responses:
        '201':
          description: Emergency address validated and stored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmergencyAddress'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: The address could not be validated against the MSAG.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples:
                      - 'emergency address could not be validated'
    get:
      summary: List emergency addresses
      description: Lists the authenticated user's saved emergency addresses.
      operationId: listEmergencyAddresses
      tags: [User Profile]
      security:
        - UserAuth: []
      responses:
        '200':
          description: A list of emergency addresses
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    enum: [list]
                  url:
                    type: string
                  next_page_url:
                    type: ['string', 'null']
                  previous_page_url:
                    type: ['string', 'null']
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EmergencyAddress'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/me/emergency-addresses/{emergency_address_id}:
    parameters:
      - name: emergency_address_id
        in: path
        required: true
        schema:
          type: string
    get:
      summary: Get an emergency address
      operationId: getEmergencyAddress
      tags: [User Profile]
      security:
        - UserAuth: []
      responses:
        '200':
          description: Emergency address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmergencyAddress'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No such emergency address
    delete:
      summary: Delete an emergency address
      operationId: deleteEmergencyAddress
      tags: [User Profile]
      security:
        - UserAuth: []
      responses:
        '204':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No such emergency address

  /v1/me/emergency-addresses/{emergency_address_id}/registered_ip:
    parameters:
      - name: emergency_address_id
        in: path
        required: true
        schema:
          type: string
    delete:
      summary: Clear an emergency address's network binding
      description: |
        Clears the address's `registered_ip`. The next time a softphone
        connects using this address, it is re-bound to the device's current
        network. Use this when a device has moved and you want the saved
        address to re-register where the device now is.
      operationId: clearEmergencyAddressRegisteredIp
      tags: [User Profile]
      security:
        - UserAuth: []
      responses:
        '204':
          description: Binding cleared
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No such emergency address

  # Call Queues

  /v1/queues:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'

    post:
      summary: Create a call queue
      description: |
        Creates a new call queue. The queue starts with no members; add agents
        with `POST /v1/queues/{queue_id}/members`.
      operationId: createQueue
      tags: [Call Queues]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateQueueRequest'
            examples:
              ringall:
                summary: Ringall queue with voicemail timeout
                value:
                  name: 'Support'
                  strategy: 'ringall'
                  timeout_seconds: 120
                  wrap_up_seconds: 15
                  announcements:
                    frequency_seconds: 30
                  max_queue_length: 50
                  timeout:
                    type: 'voicemail'
                    voicemail: 'svm_01h2xcejqtf2nbrexx3vqjhp60'
              linear:
                summary: Linear queue, no timeout
                value:
                  name: 'Tier 1'
                  strategy: 'linear'
      responses:
        '201':
          description: Queue created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Queue'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/QueueValidationFailed'

    get:
      summary: List call queues
      description: Returns a paginated list of queues for the account.
      operationId: listQueues
      tags: [Call Queues]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: List of queues
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Queue'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/queues/{queue_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/QueueId'

    get:
      summary: Get a call queue
      description: |
        Retrieves a queue by ID. The first 10 members are embedded inline under
        `members`; page through the rest via `members.next_page_url` or
        `GET /v1/queues/{queue_id}/members`.
      operationId: getQueue
      tags: [Call Queues]
      parameters:
        - name: 'expand[]'
          in: query
          description: 'Related resources to include inline. Supported values: `extensions`.'
          schema:
            type: array
            items:
              type: string
              enum: [extensions]
      responses:
        '200':
          description: Queue details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Queue'
        '404':
          $ref: '#/components/responses/QueueNotFound'

    post:
      summary: Update a call queue
      description: |
        Updates queue settings. All fields are optional.

        Send `timeout: null` to clear the timeout configuration; omit the field
        to leave it unchanged.
      operationId: updateQueue
      tags: [Call Queues]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateQueueRequest'
            examples:
              clear_timeout:
                summary: Clear the timeout configuration
                value:
                  timeout: null
              switch_strategy:
                summary: Change strategy, wrap-up, and announcement cadence
                value:
                  strategy: 'fewestcalls'
                  wrap_up_seconds: 30
                  announcements:
                    frequency_seconds: 45
      responses:
        '200':
          description: Updated queue
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Queue'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/QueueNotFound'
        '422':
          $ref: '#/components/responses/QueueValidationFailed'

    delete:
      summary: Delete a call queue
      description: Deletes a queue and removes all of its members.
      operationId: deleteQueue
      tags: [Call Queues]
      responses:
        '204':
          $ref: '#/components/responses/QueueDeleted'
        '404':
          $ref: '#/components/responses/QueueNotFound'

  /v1/queues/{queue_id}/members:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/QueueId'

    get:
      summary: List queue members
      description: Returns a paginated list of members on the queue.
      operationId: listQueueMembers
      tags: [Call Queues]
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of queue members
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/QueueMember'
        '404':
          $ref: '#/components/responses/QueueNotFound'

    post:
      summary: Add a queue member
      description: |
        Adds a user agent to the queue. Each user can be a member of a queue
        at most once; re-adding returns `409`.

        **Auto-login side effect.** On success, the user is also marked
        logged in for queue dispatch (equivalent to dialing `*45`) so they
        start receiving queue calls immediately — admins don't have to log
        agents in separately. The auto-login is conservative:

        - Agents who are already logged in are unaffected (no-op).
        - Paused agents stay paused. Adding someone to another queue while
          they're on a break doesn't end the break.
        - Logged-out agents and users who have never had queue state are
          marked available.

        Agents can still log themselves out with `*45` or via the
        `POST /v1/users/{user_id}/queue-agent` endpoint.
      operationId: addQueueMember
      tags: [Call Queues]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddQueueMemberRequest'
            examples:
              basic:
                summary: Add user with default penalty
                value:
                  user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
              ranked:
                summary: Add user with explicit penalty and position
                value:
                  user: 'user_01h2xcejqtf2nbrexx3vqjhp43'
                  penalty: 2
                  position: 3
      responses:
        '201':
          description: Member added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueueMember'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/QueueNotFound'
        '409':
          $ref: '#/components/responses/QueueMemberConflict'
        '422':
          $ref: '#/components/responses/QueueValidationFailed'

  /v1/queues/{queue_id}/members/{member_id}:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/QueueId'
      - $ref: '#/components/parameters/QueueMemberId'

    delete:
      summary: Remove a queue member
      description: Removes a member from the queue.
      operationId: removeQueueMember
      tags: [Call Queues]
      responses:
        '204':
          $ref: '#/components/responses/QueueMemberDeleted'
        '404':
          $ref: '#/components/responses/QueueMemberNotFound'

  /v1/users/{user_id}/presence:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/UserId'
    get:
      summary: Get a user's presence
      description: |
        Returns whether a user is reachable right now and whether they are on a
        call, read live from the phone system.

        The response is always freshly verified. If presence cannot be
        determined (the underlying system is unavailable or the read does not
        complete in time), the request fails with `503` rather than returning a
        stale or guessed value.
      operationId: getUserPresence
      tags: [Presence]
      responses:
        '200':
          description: The user's current presence
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserPresence'
              examples:
                available:
                  summary: Reachable and idle
                  value:
                    state: 'available'
                    notifiable: false
                    do_not_disturb: false
                    webrtc:
                      state: 'available'
                      sessions: 1
                on_call_elsewhere:
                  summary: On a call, but reachable in the browser
                  description: |
                    The user answered on another endpoint type (a desk phone),
                    so the top-level `state` is `on_call` while their WebRTC
                    session is idle and still ringable.
                  value:
                    state: 'on_call'
                    notifiable: false
                    do_not_disturb: false
                    webrtc:
                      state: 'available'
                      sessions: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such user in this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Presence could not be determined; try again
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/presence:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
    get:
      summary: Get presence for several users
      description: |
        Returns presence for an explicit set of users in one request — the
        bounded candidate set a caller already has in hand (for example, the
        possible targets of a transfer).

        This is a filtered list by id, not a paginated collection: there is no
        `limit` or cursor. The number of users per request is capped; exceeding
        the cap returns `400`. If any requested user cannot be resolved or read,
        the whole request fails (`400`/`503`) rather than returning a partial or
        guessed result.
      operationId: listPresence
      tags: [Presence]
      parameters:
        - name: 'user[]'
          in: query
          required: true
          description: The users to read presence for. Repeat the parameter for each user.
          schema:
            type: array
            items:
              type: string
              example: 'user_01h2xcejqtf2nbrexx3vqjhp42'
      responses:
        '200':
          description: Presence for each requested user, in request order
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    $ref: '#/components/schemas/ListObject'
                  url:
                    $ref: '#/components/schemas/ListUrl'
                  next_page_url:
                    $ref: '#/components/schemas/NextPageUrl'
                  previous_page_url:
                    $ref: '#/components/schemas/PreviousPageUrl'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserPresenceItem'
                required:
                  - object
                  - url
                  - next_page_url
                  - previous_page_url
                  - data
              examples:
                list:
                  summary: Bulk presence response
                  value:
                    object: 'list'
                    url: '/v1/presence'
                    next_page_url: null
                    previous_page_url: null
                    data:
                      - user: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                        state: 'on_call'
                        notifiable: false
                        do_not_disturb: true
                        webrtc:
                          state: 'on_call'
                          sessions: 1
                      - user: 'user_01h2xcejqtf2nbrexx3vqjhp43'
                        state: 'offline'
                        notifiable: true
                        do_not_disturb: false
                        webrtc:
                          state: 'offline'
                          sessions: 0
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '503':
          description: Presence could not be determined; try again
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/users/{user_id}/queue-agent:
    parameters:
      - $ref: '#/components/parameters/DialStackAccount'
      - $ref: '#/components/parameters/UserId'
    get:
      summary: Get queue-agent state
      description: |
        Returns the user's current queue-agent operational state. `status`
        is derived from the underlying timestamps: `paused_at` present
        renders as `paused`; `logged_in_at` absent renders as `logged_out`;
        otherwise `available`.

        Returns 404 if the user has never had agent state written. A user
        who logged out is *not* a 404 — the row is retained so historical
        attributes survive the logout/login cycle.
      operationId: getQueueAgent
      tags: [Call Queues]
      responses:
        '200':
          description: Current agent state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No agent state has ever been written for this user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      summary: Update queue-agent state
      description: |
        Sets a queue agent's availability state. The endpoint is idempotent:
        re-sending the current status is a 200 with no semantic change.

        Transitioning to `available` (or `paused`) from a logged-out state
        sets `logged_in_at` if it isn't already populated; subsequent flips
        between `available` and `paused` preserve the original login time.
        Transitioning to `logged_out` clears `logged_in_at`, `paused_at`,
        and `pause_reason` but retains the row so historical attributes
        survive the logout/login cycle.

        `reason` is only valid when `status` is `paused`; sending it with
        any other status returns 400.
      operationId: updateQueueAgent
      tags: [Call Queues]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetAgentStatusRequest'
            examples:
              paused_with_reason:
                summary: Pause with a reason
                value:
                  status: 'paused'
                  reason: 'break'
              available:
                summary: Mark available
                value:
                  status: 'available'
              logged_out:
                summary: Log out
                value:
                  status: 'logged_out'
      responses:
        '200':
          description: Agent status updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentStatus'
        '400':
          description: |
            Request body failed validation. Examples:
            - `status is required`
            - `status must be one of available, paused, logged_out`
            - `reason is only allowed when status is paused`
            - `reason must be 255 characters or fewer`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

tags:
  - name: Faxes
    description: |
      Send outbound faxes and review fax history. Requires account context via
      the `DialStack-Account` header or a session token.
  - name: Files
    description: |
      Upload files for use with other resources (e.g. a fax document). Requires
      account context via the `DialStack-Account` header or a session token.
  - name: Audit Logs
    description: Read-only access to audit trail entries
  - name: Presence
    description: |
      Read whether users are reachable and on a call right now, verified live
      from the phone system. Requires account context via the
      `DialStack-Account` header or a session token.
  - name: Call Queues
    description: |
      Call queue configuration, member management, and per-user agent state.
      Requires account context via `DialStack-Account` header or session token.
  - name: Accounts
    description: |
      Account (customer organization) management.
      These endpoints operate at the platform level and don't require the `DialStack-Account` header.
  - name: Sessions
    description: |
      Account session management for embedded components.
      Sessions provide scoped authentication for frontend components.
      Requires a platform API key — session tokens cannot be used to create new sessions.
  - name: Users
    description: |
      User (people) management.
      Requires account context via `DialStack-Account` header or session token.
  - name: Voicemails
    description: |
      Voicemail message management.
      Requires account context via `DialStack-Account` header or session token.
  - name: Calls
    description: |
      Call initiation, history, and real-time monitoring.
      Use POST /v1/calls to initiate click-to-call, GET /v1/calls to retrieve call history,
      and POST /v1/calls/{call_id}/listeners to stream real-time audio from active calls.
      Requires account context via `DialStack-Account` header or session token.
  - name: Phone Numbers
    description: |
      Phone number (DID) management including search, ordering, and disconnect.
      Requires account context via `DialStack-Account` header or session token.
      For porting existing numbers from another carrier, see Number Porting.
  - name: Number Porting
    description: |
      Number porting (port-in) management. Transfer existing phone numbers from another
      carrier to DialStack. The eligibility check endpoint does not require the
      `DialStack-Account` header.
  - name: AI Agents
    description: |
      AI-powered voice agent management.
      AI agents provide automated call handling with customizable instructions and FAQ responses.
      Each agent automatically manages a voice app and extension for call routing.
      Requires account context via `DialStack-Account` header.
  - name: Voice Apps
    description: |
      Voice app management for programmable voice applications.
      Voice apps handle calls via HTTP webhook notification and optional WebSocket audio streaming.
      Requires account context via `DialStack-Account` header.
  - name: Events
    description: |
      Real-time event streaming via Server-Sent Events (SSE).
      Use this endpoint to receive live notifications about calls, voicemails, and other account activity.
      Requires account context via `DialStack-Account` header or session token.
  - name: Webhook Endpoints
    description: |
      Register URLs to receive event notifications. Endpoints are mode-scoped:
      an endpoint created with a live key receives events only from live
      accounts, and one created with a test key receives events only from
      sandbox accounts — so test traffic never reaches a production URL.
      Platform-level: authenticate with your API key; no account context
      required.
  - name: Schedules
    description: |
      Business hours schedule management.
      Schedules define when the business is open based on weekly time ranges and holidays.
      A temporary hold can override the schedule calculation.
      Requires account context via `DialStack-Account` header or session token.
  - name: Dial Plans
    description: |
      Visual call routing graph management.
      Dial plans define how calls are routed through nodes (schedules, extensions).
      Multiple extensions can share the same dial plan.
      Requires account context via `DialStack-Account` header or session token.
  - name: Ring Groups
    description: |
      Ring group management for parallel dialing (call forking).
      Ring groups dial multiple members simultaneously; the first to answer wins.
      Requires account context via `DialStack-Account` header or session token.
  - name: Shared Voicemail Boxes
    description: |
      Shared voicemail box management for team or departmental voicemail.
      Shared voicemail boxes can receive messages and forward them via email.
      Requires account context via `DialStack-Account` header or session token.
  - name: Audio Clips
    description: |
      Audio clip management for hold music, IVR prompts, and other audio playback.
      Upload audio files which are validated and transcoded server-side.
      Requires account context via `DialStack-Account` header or session token.
  - name: Extensions
    description: |
      Extension (dial code) management.
      Extensions map short dial codes to routing targets.
      Requires account context via `DialStack-Account` header or session token.
  - name: Locations
    description: |
      Business location management.
      Locations represent physical offices or business premises with validated addresses.
      Requires account context via `DialStack-Account` header or session token.
  - name: Devices
    description: |
      Unified device API for deskphones, DECT bases, and DECT handsets.
      **This is the recommended way to manage devices.** Use `/v1/devices` to
      create, list, get, and delete any device type, and `/v1/devices/{id}/users`
      to assign and manage users on any device type.

      The type-specific `/v1/deskphones` and `/v1/dect-bases` endpoints are
      retained only for operations that have no unified equivalent yet
      (updates, provisioning-events, listing handsets under a specific base).
      Prefer `/v1/devices` everywhere else.

      Requires account context via `DialStack-Account` header or session token.
  - name: Deskphones
    description: |
      Deskphone-specific operations retained for the cases `/v1/devices` does
      not cover yet (updates and provisioning events). For create, list, get,
      and delete, use the unified [Devices](#tag/Devices) API — it is the
      recommended approach for all new integrations.

      Requires account context via `DialStack-Account` header or session token.
  - name: DECT Bases
    description: |
      DECT-specific operations retained for the cases `/v1/devices` does not
      cover yet (updates and listing handsets under a specific base). For
      create, list, get, and delete of bases and handsets, use the unified
      [Devices](#tag/Devices) API — it is the recommended approach for all
      new integrations.

      Requires account context via `DialStack-Account` header or session token.
  - name: Button Templates
    description: |
      Reusable programmable-key layouts that can be assigned to devices.
      A template defines button positions, types, and labels once and applies
      them to every device it is assigned to; per-device overrides can add,
      replace, or suppress individual buttons on top of (or without) a template.
      Requires account context via `DialStack-Account` header or session token.
  - name: Catalog
    description: |
      Hardware catalog listing.
      Returns available hardware items for selection during onboarding.
      Does not require account context.
  - name: Hardware Orders
    description: |
      Hardware order management.
      Orders contain line items representing hardware selections (model + quantity).
      Requires account context via `DialStack-Account` header or session token.
  - name: WebRTC
    description: |
      WebRTC configuration for softphone applications.
      Provides ICE server credentials for establishing peer connections.
      The signalling protocol is documented in the [WebRTC Signalling Protocol](/webrtc/protocol).
      Requires a user token.
  - name: User Profile
    description: |
      User-scoped REST endpoints for softphone and user-facing applications.
      Returns data scoped to the authenticated user (own call history, voicemails,
      presence, device registration). Requires a user token.
  - name: Quality
    description: |
      Call quality metrics and device registration health.
      Aggregate MOS/packet-loss summaries, worst-call lookups, and live device
      registration state for monitoring and troubleshooting.
      Requires account context via `DialStack-Account` header or session token.

x-tagGroups:
  - name: Accounts & Users
    tags:
      - Accounts
      - Sessions
      - Users
      - User Profile
      - Extensions
      - Presence
  - name: Calls
    tags:
      - Calls
      - Voicemails
      - Faxes
      - Files
      - Events
      - WebRTC
  - name: Routing
    tags:
      - Dial Plans
      - Ring Groups
      - Call Queues
      - Schedules
      - Shared Voicemail Boxes
      - Audio Clips
      - Phone Numbers
      - Number Porting
      - Locations
  - name: AI & Voice Apps
    tags:
      - AI Agents
      - Voice Apps
  - name: Devices & Hardware
    tags:
      - Devices
      - Deskphones
      - DECT Bases
      - Button Templates
      - Hardware Orders
      - Catalog
  - name: Platform
    tags:
      - Audit Logs
      - Quality
      - Webhook Endpoints
