asyncapi: 3.0.0
info:
  title: DialStack Media WebSocket Protocol
  version: 2.0.0
  description: |
    Real-time audio streaming protocol over WebSocket.

    ## Overview

    DialStack connects to a platform-provided WSS URL, sends a `begin`
    handshake, exchanges `audio` frames, and sends an `end` message before
    closing. The same wire protocol is used for every session; the enabled
    features depend on which REST API created it.

    ## Features

    ### Outbound audio

    Audio from DialStack to the platform is always present. Each `audio`
    message carries a timestamp (milliseconds from session start) and a
    base64-encoded μ-law payload.

    ### Inbound audio (optional)

    The platform may send `audio` messages back to DialStack to be played to
    the caller. When this is not enabled, DialStack does not read from the
    WebSocket.

    ### Channel tagging (optional)

    Outbound `audio` messages may carry a `channel` field (`caller` or
    `callee`) when both sides of the call are streamed on the same
    connection. When the field is absent, the stream is a single mixed
    channel.

    ### Session identity

    The `begin` message carries exactly one of `voice_app_id` or
    `listener_id`, identifying the resource that created the session.
    Consumers can branch on which field is present to select per-resource
    behavior.

    ### Graceful end

    Before closing the WebSocket, DialStack sends an `end` message with a
    `reason` (`call_ended`, `deleted`, `error`) so the consumer can
    distinguish normal termination from errors.

    ## Connection Flow

    1. Platform provides a WSS URL when creating the session
    2. DialStack connects and sends `begin` with session metadata and audio format
    3. Audio flows via `audio` messages
    4. DialStack sends `end` with a reason
    5. DialStack closes the WebSocket

    ## Audio Format

    - Encoding: μ-law (G.711)
    - Sample rate: 8000 Hz
    - Channels: 1 (mono)
    - Chunk size: ~20ms (160 bytes before base64 encoding)
    - Bandwidth: ~8 KB/second per channel

    ## Connection Behavior

    - DialStack initiates the TLS WebSocket connection
    - Ping/pong keepalive every 30 seconds
    - If the connection drops during an active call, DialStack attempts reconnection (up to 3 times)
    - Closing the WebSocket stops the audio session; the effect on the underlying call depends on the session type

    ## Related Documentation
    - [REST API Reference](/api) — endpoints that create sessions
    - [Download AsyncAPI Spec](/api/asyncapi.yaml) — raw YAML specification

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

servers:
  platform:
    host: ai.platform.example.com
    pathname: /voice
    protocol: wss
    description: |
      Platform-provided WebSocket URL, supplied when the session is created
      via the REST API. DialStack connects to this URL and streams audio.

channels:
  mediaStream:
    address: /
    description: |
      Media stream channel. DialStack connects to the platform's WebSocket URL
      and exchanges JSON messages for session control and audio data. Outbound
      audio is always streamed; inbound audio is accepted when the session was
      created with that feature enabled.
    messages:
      begin:
        name: begin
        title: Session Begin
        summary: First message after WebSocket connection, containing session metadata
        contentType: application/json
        payload:
          $ref: '#/components/schemas/BeginMessage'
      audioFromDialStack:
        name: audio
        title: Audio from DialStack
        summary: |
          Audio sent from DialStack to the platform. May carry a per-chunk
          `channel` tag when multi-leg audio is streamed on the same connection.
        contentType: application/json
        payload:
          $ref: '#/components/schemas/AudioMessageWithTimestamp'
      audioToDialStack:
        name: audio
        title: Audio to DialStack
        summary: |
          Audio to play to the caller, sent from the platform to DialStack.
          Accepted only when the session was created with inbound audio enabled.
        contentType: application/json
        payload:
          $ref: '#/components/schemas/AudioMessage'
      end:
        name: end
        title: Session End
        summary: Sent before DialStack closes the WebSocket, with a reason
        contentType: application/json
        payload:
          $ref: '#/components/schemas/EndMessage'

operations:
  sendBegin:
    action: send
    channel:
      $ref: '#/channels/mediaStream'
    summary: Session start notification
    description: |
      Sent by DialStack immediately after the WebSocket connection is
      established. Contains session metadata (exactly one of `voice_app_id`
      or `listener_id`), optional pass-through `metadata`, and the audio
      format specification.
    messages:
      - $ref: '#/channels/mediaStream/messages/begin'

  sendAudio:
    action: send
    channel:
      $ref: '#/channels/mediaStream'
    summary: Audio to platform
    description: |
      Audio sent by DialStack to the platform. Each message carries a
      timestamp (milliseconds from session start). When multi-leg audio is
      streamed on the same connection, messages also carry a `channel` tag
      (`caller` or `callee`); otherwise the tag is omitted.
    messages:
      - $ref: '#/channels/mediaStream/messages/audioFromDialStack'

  receiveAudio:
    action: receive
    channel:
      $ref: '#/channels/mediaStream'
    summary: Audio to caller
    description: |
      Audio to play to the caller, sent by the platform to DialStack. Accepted
      only when the session was created with inbound audio enabled; otherwise
      DialStack does not read from the WebSocket.
    messages:
      - $ref: '#/channels/mediaStream/messages/audioToDialStack'

  sendEnd:
    action: send
    channel:
      $ref: '#/channels/mediaStream'
    summary: Session end
    description: |
      Sent by DialStack before closing the WebSocket. The `reason` field
      distinguishes a normal hangup from an explicit deletion or an error.
    messages:
      - $ref: '#/channels/mediaStream/messages/end'

components:
  schemas:
    BeginMessage:
      type: object
      description: |
        Session start message with call metadata and audio format. Exactly one
        of `voice_app_id` or `listener_id` is present.
      required:
        - event
        - call_id
        - account_id
        - audio_format
      properties:
        event:
          type: string
          const: begin
          description: Event type identifier
        call_id:
          type: string
          description: Unique identifier for this call
          examples:
            - call_01h2xcejqtf2nbrexx3vqjhp45
        account_id:
          type: string
          description: Account identifier
          examples:
            - acct_01h2xcejqtf2nbrexx3vqjhp41
        audio_format:
          $ref: '#/components/schemas/AudioFormat'
        voice_app_id:
          type: string
          description: |
            Session identifier. Mutually exclusive with `listener_id` —
            exactly one of the two is present on every `begin`.
          examples:
            - voiceapp_01h2xcejqtf2nbrexx3vqjhp42
        listener_id:
          type: string
          description: |
            Session identifier. Mutually exclusive with `voice_app_id` —
            exactly one of the two is present on every `begin`.
          examples:
            - lstn_01h2xcejqtf2nbrexx3vqjhp50

    AudioFormat:
      type: object
      description: Audio encoding specification
      required:
        - encoding
        - sample_rate
        - channels
      properties:
        encoding:
          type: string
          const: audio/x-mulaw
          description: Audio encoding (μ-law G.711)
        sample_rate:
          type: integer
          const: 8000
          description: Sample rate in Hz
        channels:
          type: integer
          const: 1
          description: Number of audio channels (mono)

    AudioMessageWithTimestamp:
      type: object
      description: |
        Audio from DialStack to the platform. Carries an optional `channel`
        tag when multi-leg audio is streamed on the same connection.
      required:
        - event
        - timestamp
        - payload
      properties:
        event:
          type: string
          const: audio
          description: Event type identifier
        timestamp:
          type: integer
          description: Milliseconds from session start
          examples:
            - 0
            - 20
            - 40
        channel:
          type: string
          enum: [caller, callee]
          description: |
            Which party's audio this chunk contains. Present only when
            multi-leg audio is streamed on the same connection; otherwise
            omitted (the stream is a single mixed channel).
        payload:
          type: string
          format: byte
          description: Base64-encoded μ-law audio data (~160 bytes per 20ms chunk)

    AudioMessage:
      type: object
      description: |
        Audio data to play to caller (Platform → DialStack). Accepted only
        when the session was created with inbound audio enabled.
      required:
        - event
        - payload
      properties:
        event:
          type: string
          const: audio
          description: Event type identifier
        payload:
          type: string
          format: byte
          description: Base64-encoded μ-law audio data

    EndMessage:
      type: object
      description: |
        Sent by DialStack before closing the WebSocket.
      required:
        - event
        - reason
      properties:
        event:
          type: string
          const: end
          description: Event type identifier
        reason:
          type: string
          enum: [call_ended, deleted, error]
          description: |
            Why the session ended:
            - `call_ended`: the call was hung up
            - `deleted`: the session was closed via the API
            - `error`: an internal error occurred
