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/auth/token` and use the returned user token.

    Example: `Authorization: Bearer eyJhbGciOiJFZERTQSIs...`

    User tokens are scoped to a single user within an account and grant access to the WebRTC
    signalling channel, call history, voicemails, and presence. See the
    [Authentication guide](/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/auth/token and include it in the Authorization header:
        `Authorization: Bearer eyJhbGciOiJFZERTQSIs...`

        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 1 hour. Use POST /v1/auth/refresh to extend.

  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'

    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'

    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

    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
      description: Filter by location status
      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'

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

    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'

    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: Operation would create a routing loop
      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'

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

    AudioClipDeleted:
      description: Audio clip 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'

    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

    # --- User Authentication ---

    AuthTokenRequest:
      type: object
      required:
        - grant_type
        - account_id
        - subject_token
        - subject_token_type
      properties:
        grant_type:
          type: string
          enum: [token_exchange]
          description: Authentication method. Currently only `token_exchange` is supported.
        account_id:
          type: string
          description: Account the user belongs to
          examples:
            - acct_01h2xcejqtf2nbrexx3vqjhp41
        subject_token:
          type: string
          description: Your platform's JWT for the user being authenticated
        subject_token_type:
          type: string
          description: Token type URI
          examples:
            - 'urn:ietf:params:oauth:token-type:jwt'

    AuthTokenResponse:
      type: object
      required:
        - token
        - token_type
        - expires_at
        - expires_in
        - user
      properties:
        token:
          type: string
          description: User token for authenticating client-side requests
          examples:
            - eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
        token_type:
          type: string
          const: bearer
          description: Always `bearer`
        expires_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the token expires
        expires_in:
          type: integer
          description: Seconds until the token expires
          examples:
            - 3600
        user:
          $ref: '#/components/schemas/AuthUser'
    AuthUser:
      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_id:
          type: string
          description: Account the user belongs to

    AuthRefreshRequest:
      type: object
      required:
        - token
      properties:
        token:
          type: string
          description: Current user token to refresh

    AuthRevokeRequest:
      type: object
      required:
        - token
      properties:
        token:
          type: string
          description: User token to revoke

    # --- 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:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
          description: STUN or TURN server URL(s)
          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)

    # --- 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_id:
          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_id:
          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)

    EmergencyAddress:
      type: object
      description: Validated emergency address for E911
      required:
        - street
        - city
        - state
        - zip
        - country
        - validated
      properties:
        street:
          type: string
          description: Street address
          examples:
            - '123 Main St'
        street2:
          type: ['string', 'null']
          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'
        zip:
          type: string
          examples:
            - '94105'
        country:
          type: string
          description: Two-letter country code (ISO 3166-1 alpha-2)
          examples:
            - 'US'
        validated:
          type: boolean
          description: Whether the address has been validated against the MSAG
        updated_at:
          type: string
          format: date-time

    EmergencyAddressRequest:
      type: object
      required:
        - street
        - city
        - state
        - zip
        - country
      properties:
        street:
          type: string
          description: Street address
        street2:
          type: ['string', 'null']
          description: Suite, floor, apartment, or room number
        city:
          type: string
        state:
          type: string
          description: Two-letter state/province code
        zip:
          type: string
        country:
          type: string
          description: Two-letter country code (ISO 3166-1 alpha-2)

    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]
          description: Type of 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, fulfilled, cancelled]
          description: Order lifecycle status
        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
      properties:
        id:
          type: string
          description: Hardware order item ID
        hardware_catalog_id:
          type: string
          description: Reference to hardware catalog item
        quantity:
          type: integer
          description: Number of units
          minimum: 1
          maximum: 100
        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_id, quantity]
            properties:
              hardware_catalog_id:
                type: string
                description: ID of the hardware catalog item
              quantity:
                type: integer
                minimum: 1
                maximum: 100

    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_id:
          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.
        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.
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - config
        - created_at

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

    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'
        account_role:
          type: [string, 'null']
          description: The user's role within the account (null for regular users)
          enum: [account_admin, null]
          example: null
        outbound_caller_id_did_id:
          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'
        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

    UserConfig:
      type: object
      description: User-level configuration settings
      properties:
        voicemail_notifications:
          $ref: '#/components/schemas/VoicemailNotificationConfig'
        find_me_follow_me:
          $ref: '#/components/schemas/FMFM'

    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_id`.
        fallback_target_id:
          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.'
      required:
        - id
        - owner
        - from_number
        - duration_seconds
        - format
        - audio_url
        - is_read
        - created_at

    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

    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.
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
        user_id:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - type: 'null'
          description: The user associated with this call
        endpoint_id:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - type: 'null'
          description: The endpoint (device) that handled this call
        did_id:
          oneOf:
            - $ref: '#/components/schemas/EntityId'
            - type: 'null'
          description: The DID (phone number) used for this call
        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
          maxLength: 20
          description: Caller's phone number
          example: '+14155551234'
        from_label:
          type: [string, 'null']
          description: Display label for the caller, 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: 'Front Desk'
        to_number:
          type: string
          maxLength: 20
          description: Recipient's phone number
          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
          example: '2025-10-18T14:30:05Z'
        ended_at:
          type: [string, 'null']
          format: date-time
          description: When the call ended
          example: '2025-10-18T14:35:30Z'
        duration_seconds:
          type: [integer, 'null']
          minimum: 0
          description: Call duration in seconds (from answer to hangup)
          example: 325
        status:
          type: string
          enum: [completed, no-answer, busy, failed, voicemail]
          description: Final call status
          example: 'completed'
        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.'
        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 for unanswered calls.
          items:
            $ref: '#/components/schemas/QualityMetricLeg'
      required:
        - id
        - direction
        - from_number
        - to_number
        - started_at
        - status

    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_id:
          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

    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...'
      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_id:
          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...'
      required:
        - voicemail_id
        - 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
        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'
        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_id:
          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 if no routing is configured
          example: null
        created_at:
          allOf:
            - $ref: '#/components/schemas/CreatedAt'
        updated_at:
          allOf:
            - $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - phone_number
        - status
        - outbound_enabled
        - routing_target
        - created_at
        - updated_at

    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-local
          description: |
            When the hold expires. Interpreted in the schedule's timezone.
          example: '2025-12-20T17:00:00'
      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
          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
          example: 'Jane Doe'
        config:
          $ref: '#/components/schemas/AccountConfig'

    CreateAccountRequest:
      $ref: '#/components/schemas/AccountRequest'

    UpdateAccountRequest:
      allOf:
        - $ref: '#/components/schemas/AccountRequest'
        - type: object
          properties:
            default_outbound_did_id:
              type: string
              description: ID of the phone number to use as the default outbound caller ID. Send null to clear.
            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_id:
              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'
            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'

    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.

            - `type`: `"transfer"`
            - `extension`: Extension number to transfer to

            **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
        extension:
          type: string
          description: Extension number to transfer to
          example: '100'
      required:
        - type
        - extension

    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

    CreateListenerRequest:
      type: object
      description: Request to create a listener on an active call
      properties:
        url:
          type: string
          format: uri
          description: WebSocket URL (wss://) where DialStack will stream audio
          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_id:
          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_id
        - 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
        - extension_number

    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-local
          description: |
            When the hold expires. Interpreted in the schedule's timezone.
            If not provided, defaults to midnight (00:00:00) the next day.
          example: '2025-12-20T17:00:00'
      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 count as closed)
        - `internal_dial` — rings an internal target (user, ring group, dial plan, voice app, 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)
        - `sound_clip` — plays an audio clip then routes to the next node

        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, sound_clip]
          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`
            - `sound_clip` → `SoundClipNodeConfig`

            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/SoundClipNodeConfig'
      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 or closed (holidays are
        treated as closed).

        Exit targets are optional — if omitted or null, the call terminates.
      properties:
        schedule_id:
          type: string
          description: ID of the schedule to check (returned by the Schedules API)
          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 or on holiday (null = terminate call)
          example: 'voicemail'

    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_id` may reference any of the following resources:

        - a **user** (returned by the Users API)
        - a **ring group** (returned by the Ring Groups API)
        - a nested **dial plan** (returned by the Dial Plans API)
        - a **voice app** (returned by the Voice Apps API)
        - a **shared voicemail** (returned by the Voicemails API)

        The runtime dispatches on the kind of resource the `target_id` 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.

        **Editor note:** the hosted dial-plan editor surfaces some of these
        shapes as distinct visual nodes ("Voicemail", "Voice App") even though
        they are serialized as `internal_dial`. The API shape is the same.
      properties:
        target_id:
          type: string
          description: ID of the internal target to dial
          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; must be set before the node will
            dial.
          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_id:
          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.
          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
          minItems: 1
          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'
      required:
        - digit

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

    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, null]
          description: Action to take when the ring group times out
          example: 'voicemail'
        timeout_target:
          type: [string, 'null']
          description: ID of the user or shared voicemail box to ring or send to voicemail when the ring group times out. 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_id:
          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_id
        - 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]
          description: Action to take when the ring group times out
          example: 'voicemail'
        timeout_target:
          type: string
          description: ID of the user or shared voicemail box to ring or send to voicemail on timeout. 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
          enum: [ring_user, voicemail]
          description: Action to take when the ring group times out
          example: 'voicemail'
        timeout_target:
          type: string
          description: ID of the user or shared voicemail box to ring or send to voicemail on timeout. 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'

    # Shared Voicemail Box Schemas

    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_id:
          type: string
          description: The user to assign to the deskphone
          example: '01h2xcejqtf2nbrexx3vqjhp42'
      required:
        - user_id

    DeviceUserAssignment:
      type: object
      description: A user assigned to a device
      properties:
        user_id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: The assigned user
              example: '01h2xcejqtf2nbrexx3vqjhp42'
        device_id:
          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_id
        - device_id
        - 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_id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Parent device
              example: 'dev_01h2xcejqtf2nbrexx3vqjhp50'
        endpoint_id:
          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_id
        - endpoint_id
        - line_number
        - created_at
        - updated_at

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

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

    # Device Settings Schemas

    DeviceSettings:
      type: [object, 'null']
      description: |
        Two-tier device settings with vendor-agnostic abstractions and vendor-specific overrides.
        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. The `vendor_overrides`
        field provides an escape hatch for vendor-specific parameters not yet covered by
        abstractions.
      properties:
        abstractions:
          $ref: '#/components/schemas/AbstractSettings'
        vendor_overrides:
          type: object
          description: |
            Vendor-specific key-value overrides applied directly to the generated configuration.
            Keys must match the vendor's allowlisted parameter names.
          additionalProperties:
            type: string
          example:
            ntp_server: 'time.nist.gov'

    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'
        line_keys:
          type: array
          description: |
            Programmable function key assignments. Each entry configures a physical
            button on the device. When provided, replaces all existing line key settings.
          items:
            $ref: '#/components/schemas/LineKey'
          example:
            - position: 1
              type: blf
              label: 'Reception'
              value: '100'
            - position: 2
              type: speed_dial
              label: 'Support'
              value: '+15551234567'
            - position: 3
              type: voicemail
              label: 'Voicemail'
              value: '*97'

    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
        ntp_server:
          type: string
          description: NTP server address for time synchronization
        rtcp_enabled:
          type: boolean
          description: Enable RTCP for call quality reporting

    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

    LineKey:
      type: object
      description: A programmable function key assignment
      properties:
        position:
          type: integer
          description: Key position (1-based index)
          minimum: 1
          example: 1
        type:
          type: string
          enum:
            - blf
            - speed_dial
            - dtmf
            - line
            - voicemail
            - url
            - multicast
            - conference
            - transfer
            - forward
            - park
            - intercom
            - dnd
            - record_toggle
          description: |
            Key function type:
            - `blf` — Busy Lamp Field, monitors another extension's presence
            - `speed_dial` — dials a preconfigured number
            - `dtmf` — sends a DTMF sequence
            - `line` — SIP line appearance
            - `voicemail` — one-touch voicemail access
            - `url` — triggers an HTTP request
            - `multicast` — multicast RTP paging
            - `conference` — conference initiation
            - `transfer` — call transfer
            - `forward` — call forwarding activation
            - `park` — call park/retrieve
            - `intercom` — intercom auto-answer call
            - `dnd` — Do Not Disturb toggle
            - `record_toggle` — call recording toggle
        label:
          type: string
          description: Display label shown on the device screen
          example: 'Reception'
        value:
          type: string
          description: |
            Value depends on the key type: extension number (blf, speed_dial),
            phone number, DTMF sequence, URL, or multicast address.
          example: '100'

    # 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_id:
          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_id
        - event_type
        - created_at

    # Device Schemas

    Device:
      type: object
      description: |
        A provisioned device (desk phone or DECT base station).
        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]
          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'
        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_id:
          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 base only)
        handsets:
          type: array
          description: Paired handsets (DECT base only)
          items:
            $ref: '#/components/schemas/DECTHandset'
      required:
        - id
        - type
        - mac_address
        - vendor
        - status
        - created_at
        - updated_at

    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
        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'
        overrides:
          $ref: '#/components/schemas/DeviceSettings'
        multicell_role:
          type: string
          enum: [single, data_master, secondary]
          description: Multicell deployment role (DECT base only, defaults to single)
        base_id:
          type: string
          description: Parent DECT base identifier (required for DECT handsets)
          example: '01h2xcejqtf2nbrexx3vqjhp50'
        ipei:
          type: string
          description: DECT handset IPEI (required for DECT handsets)
          example: '123456789012345'
        display_name:
          type: string
          description: Optional display name for a DECT handset
          example: 'Front Desk'
      required:
        - type

    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'
        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_id:
          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_id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Parent DECT base station
        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
        - base_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

    HandsetLine:
      type: object
      description: A SIP line assignment on a DECT handset
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Handset line identifier
        handset_id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: Parent handset
        endpoint_id:
          allOf:
            - $ref: '#/components/schemas/EntityId'
            - description: The SIP endpoint assigned to this line
        display_name:
          type: [string, 'null']
          description: Optional display name override
        endpoint:
          type: [object, 'null']
          description: The associated SIP endpoint (included on list responses)
          properties:
            id:
              allOf:
                - $ref: '#/components/schemas/EntityId'
                - description: Endpoint identifier
            user_id:
              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'
        created_at:
          $ref: '#/components/schemas/CreatedAt'
        updated_at:
          $ref: '#/components/schemas/UpdatedAt'
      required:
        - id
        - handset_id
        - endpoint_id
        - created_at
        - updated_at

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

    # 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'
        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
        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
      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
      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_id:
          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_id
        - dial_string

    # 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_id:
          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_id
        - client_secret
        - expires_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'

    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_id:
          type: [string, 'null']
          description: The primary phone number assigned to this location
        status:
          type: string
          enum: [active, inactive]
          description: Operational status of the location
          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'
        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_id:
          type: string
          description: ID of the primary phone number for this location
      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_id:
          type: string
          description: ID of the primary phone number for this location
        status:
          type: string
          enum: [active, inactive]
          description: Operational status of the location

    # 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
            - 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_id:
          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_id
        - 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
      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

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'
              with_config:
                summary: Account with configuration
                value:
                  email: 'contact@example.com'
                  config:
                    region: 'US'
                    timezone: 'America/New_York'
                    extension_length: 4
      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
      description: |
        List all accounts for the authenticated platform.
        Results are returned in reverse chronological order (newest first).

      operationId: listAccounts
      tags: [Accounts]
      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
      description: |
        Retrieve a specific account by ID.

      operationId: getAccount
      tags: [Accounts]
      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'
    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'

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

  # 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
      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]
      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_id: 'vm_01h2xcejqtf2nbrexx3vqjhp45'
                    status: 'completed'
                    text: 'Hi, this is John Smith calling about my appointment tomorrow...'
                pending:
                  summary: Pending transcription
                  value:
                    voicemail_id: '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/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_id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                  dial_string: '+15551234567'
              local:
                summary: Call a local number
                value:
                  user_id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                  dial_string: '5551234567'
              extension:
                summary: Call an extension
                value:
                  user_id: '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 recipient's phone number (exact match)
          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
      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_id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                        endpoint_id: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
                        did_id: 'did_01h2xcejqtf2nbrexx3vqjhp46'
                        direction: 'inbound'
                        from_number: '+14155551234'
                        to_number: '+14155559876'
                        started_at: '2025-10-18T14:30:00Z'
                        answered_at: '2025-10-18T14:30:05Z'
                        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]
      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_id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                    endpoint_id: 'ep_01h2xcejqtf2nbrexx3vqjhp43'
                    did_id: 'did_01h2xcejqtf2nbrexx3vqjhp46'
                    direction: 'inbound'
                    from_number: '+14155551234'
                    to_number: '+14155559876'
                    started_at: '2025-10-18T14:30:00Z'
                    answered_at: '2025-10-18T14:30:05Z'
                    ended_at: '2025-10-18T14:35:30Z'
                    duration_seconds: 325
                    status: 'completed'
                missed_call:
                  summary: Missed call (no answer)
                  value:
                    id: 'call_01h2xcejqtf2nbrexx3vqjhp47'
                    user_id: 'user_01h2xcejqtf2nbrexx3vqjhp42'
                    did_id: 'did_01h2xcejqtf2nbrexx3vqjhp46'
                    direction: 'inbound'
                    from_number: '+14155551234'
                    to_number: '+14155559876'
                    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", "extension": "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'
                      extension: '100'
              attach_then_transfer:
                summary: AI with human fallback
                value:
                  actions:
                    - type: 'attach'
                      url: 'wss://ai.platform.example.com/voice'
                    - type: 'transfer'
                      extension: '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}/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 (e.g., missing url, unsupported channel)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      summary: List listeners
      description: |
        List all active listeners on a call.

      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.

      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.

      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. Released numbers cannot be reactivated.
                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_id:
                  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_visibility:
                  type: string
                  enum: [PUBLIC, PRIVATE]
                  default: 'PUBLIC'
                  description: Whether the caller ID name is publicly visible
            examples:
              disable_outbound:
                summary: Disable outbound calling
                value:
                  outbound_enabled: false
              deactivate:
                summary: Deactivate a phone number
                value:
                  status: inactive
              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.

      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_id:
                  type: string
                  description: ID of the account to reassign the phone number to
              required:
                - target_account_id
            examples:
              reassign:
                summary: Reassign to another account
                value:
                  target_account_id: '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 routing target
      description: |
        Set or clear the [routing target](#section/Routing-Targets) for a phone number.
        The routing target determines where inbound calls to this number are directed.

        Send a target ID to route calls to that target, or `null` to clear
        the current routing. Omitting the `routing_target` key 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, or null to clear routing
            examples:
              set_target:
                summary: Route calls to a target
                value:
                  routing_target: 'user_01h2xcejqtf2nbrexx3vqjhp48'
              clear_target:
                summary: Clear routing
                value:
                  routing_target: null
      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'

  # 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_id` | 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) |

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

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.initiated"` |
        | `account_id` | 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_id` | string | User who initiated the call |

        ### 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_id` | string | Account owning the user being rung |
        | `call_id` | string | Call detail record identifier |
        | `user_id` | 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.answered
        Sent when a call is answered.

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.answered"` |
        | `account_id` | 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 | ISO 8601 timestamp |

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

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.end"` |
        | `account_id` | 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 |
        | `ended_at` | string | ISO 8601 timestamp |

        ### call.transfer
        Sent when a call is transferred.

        | Field | Type | Description |
        |-------|------|-------------|
        | `event` | string | Always `"call.transfer"` |
        | `account_id` | 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) |

      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'

  # 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-20T17:00:00'
              close_until_tomorrow:
                summary: Close until midnight (default)
                value:
                  value: false
              force_open:
                summary: Force open temporarily
                value:
                  value: true
                  until: '2025-12-20T12:00:00'
      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-20T17:00:00'
                    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_id: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                        open: 'reception'
                        closed: 'voicemail'
                    - id: 'reception'
                      type: 'internal_dial'
                      config:
                        target_id: 'user_01h2xcejqtf2nbrexx3vqjhp45'
                        timeout: 30
                        next: 'voicemail'
                    - id: 'voicemail'
                      type: 'internal_dial'
                      config:
                        target_id: '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_id: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                          open: 'reception'
                          closed: 'voicemail'
                      - id: 'reception'
                        type: 'internal_dial'
                        config:
                          target_id: 'user_01h2xcejqtf2nbrexx3vqjhp45'
                          timeout: 30
                          next: 'voicemail'
                      - id: 'voicemail'
                        type: 'internal_dial'
                        config:
                          target_id: '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_id: 'sched_01h2xcejqtf2nbrexx3vqjhp50'
                        open: 'reception'
                        closed: 'after_hours'
                    - id: 'reception'
                      type: 'internal_dial'
                      config:
                        target_id: 'user_01h2xcejqtf2nbrexx3vqjhp45'
                        timeout: 30
                    - id: 'after_hours'
                      type: 'internal_dial'
                      config:
                        target_id: '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

  /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. Only the target can be changed;
        to change the extension number, delete and recreate the extension.

      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'
      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'
        '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'

  # 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 `base_id` and `ipei`
      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'
              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_id: '01h2xcejqtf2nbrexx3vqjhp50'
                  ipei: '123456789012345'
                  display_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 all physical devices (deskphones and DECT base stations).
        Use the optional `type` filter to return only deskphones or only DECT base stations.
        Results are returned in reverse chronological order (newest first).

      operationId: listDevices
      tags: [Devices]
      parameters:
        - $ref: '#/components/parameters/DeviceTypeFilter'
        - $ref: '#/components/parameters/Limit'
      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:
                oneOf:
                  - $ref: '#/components/schemas/Device'
                  - $ref: '#/components/schemas/DECTHandset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DeviceNotFound'

    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
        removes all its handsets and their user assignments.

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

  # 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.
                    items:
                      type: object
                      required: [line_number, status]
                      properties:
                        line_number:
                          type: integer
                          example: 1
                        status:
                          type: string
                          enum: [delivered, not_registered, unreachable, error]
        '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_id: '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 (user already assigned or maximum lines reached)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                already_assigned:
                  value:
                    error: 'user is already assigned to this device'
                max_lines:
                  value:
                    error: 'device has reached the maximum of 24 lines'

    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_id: '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_id: 'dev_01h2xcejqtf2nbrexx3vqjhp50'
                    endpoint_id: '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_id: '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_id` 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_id: '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 or display name.

      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'
      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_id: '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_id: 'decth_01h2xcejqtf2nbrexx3vqjhp70'
                    endpoint_id: '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 (name, address, primary phone number, or status).

      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'

  /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 primary phone number assigned.
        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: Location is missing a primary phone number
          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 in draft status with the given items.
        Each item references a hardware catalog entry with a quantity.
      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'
        '409':
          description: Duplicate catalog item in order
    get:
      summary: List hardware orders
      description: |
        Returns all hardware orders for the account, including embedded items.
      operationId: listHardwareOrders
      tags: [Hardware Orders]
      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
      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.
        Sends the complete desired state of items — existing items are replaced atomically.
      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 not in draft status

  # --- User Authentication ---

  /v1/auth/token:
    post:
      summary: Create a user token
      description: |
        Exchanges your platform's JWT for a DialStack user token. User tokens grant
        access to the WebRTC signalling channel and user-scoped REST endpoints (`/v1/me/*`).

        Your platform must be configured for token exchange during onboarding.
        DialStack verifies your JWT using the JWKS URL and issuer configured for
        your platform, then maps the subject claim to a DialStack user.

        Requires a platform API key. User tokens cannot be used to create new tokens.
      operationId: createUserToken
      tags: [User Authentication]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthTokenRequest'
      responses:
        '200':
          description: User token created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokenResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UserNotFound'

  /v1/auth/refresh:
    post:
      summary: Refresh a user token
      description: |
        Extends the lifetime of an existing user token. Returns a new token with
        a fresh expiry. The original token remains valid until its own expiry.

        Refresh proactively (e.g., 5 minutes before expiry) to avoid interrupting
        active WebRTC sessions.
      operationId: refreshUserToken
      tags: [User Authentication]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthRefreshRequest'
      responses:
        '200':
          description: Token refreshed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokenResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/auth/revoke:
    post:
      summary: Revoke a user token
      description: |
        Immediately invalidates a user token. Any active WebRTC session using
        this token is disconnected.

        Call this when the user logs out of your application.
      operationId: revokeUserToken
      tags: [User Authentication]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthRevokeRequest'
      responses:
        '204':
          description: Token revoked
        '401':
          $ref: '#/components/responses/Unauthorized'

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

  # --- User-Scoped Endpoints ---

  /v1/me:
    get:
      summary: Get current user profile
      description: |
        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: |
        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: |
        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: |
        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/emergency-address:
    get:
      summary: Get emergency address
      description: |
        Returns the authenticated user's registered emergency address for E911.
        Returns 404 if no address has been registered.
      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 emergency address registered
    put:
      summary: Set emergency address
      description: |
        Registers or updates the user's emergency address for E911. The address
        is validated against the MSAG (Master Street Address Guide) before saving.
        If validation fails, the response includes suggestions.

        An emergency address is required before the user can place calls. Attempting
        to dial without a validated address returns an error on the signalling channel.
      operationId: setEmergencyAddress
      tags: [User Profile]
      security:
        - UserAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmergencyAddressRequest'
      responses:
        '200':
          description: Emergency address validated and saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmergencyAddress'
        '400':
          description: Address validation failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    description: Error message
                    examples:
                      - 'Address could not be validated'
                  code:
                    type: string
                    enum: [address_validation_failed]
                  suggestions:
                    type: array
                    description: Suggested corrected addresses (if available)
                    items:
                      $ref: '#/components/schemas/EmergencyAddressRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

tags:
  - name: Audit Logs
    description: Read-only access to audit trail entries
  - 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: 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: 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: User Authentication
    description: |
      User-level authentication for softphones and user-facing applications.
      Authenticate end users to obtain tokens for WebRTC signalling and user-scoped
      REST endpoints. Supports token exchange, managed credentials, and SSO.
      Requires a platform API key to create tokens.
  - 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
      - User Authentication
      - Extensions
  - name: Calls
    tags:
      - Calls
      - Voicemails
      - Events
      - WebRTC
  - name: Routing
    tags:
      - Dial Plans
      - Ring Groups
      - 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
      - Hardware Orders
      - Catalog
  - name: Platform
    tags:
      - Audit Logs
      - Quality
