> ## Documentation Index
> Fetch the complete documentation index at: https://developer.suki.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Audio Streaming

> WebSocket endpoint for real-time audio streaming during ambient sessions

Use this API to stream audio to the speech service over a WebSocket connection for ambient and Form filling sessions.

For more information on how to handle the **handshake**, **wire format**, **message order**, and **error handling**, refer to the [Ambient audio streaming](/documentation/ambient-audio-streaming) guide.

## Prerequisites

Complete these steps **before** opening the WebSocket.

<Warning>
  Opening `/ws/stream` before the session and context are ready often leads to handshake failures or a broken stream.
</Warning>

* **Authenticate** and obtain `sdp_suki_token`
* **Create an ambient session** with <Badge color="blue" size="sm">POST</Badge> [`/api/v1/ambient/session/create`](/api-reference/ambient-sessions/create). A successful create returns **201 Created**; keep the `ambient_session_id` you used or received.
* **Seed session context** with <Badge color="blue" size="sm">POST</Badge> [`/api/v1/ambient/session/{ambient_session_id}/context`](/api-reference/ambient-sessions/context). Send the JSON body your integration requires (see that endpoint for the full schema).
* **Authenticate and open the WebSocket** on `wss://sdp.suki-stage.com/ws/stream`. To stream audio, you must first establish an **authenticated WebSocket connection**. The authentication method you use depends on your client types: browser or non-browser.

## Browser clients

When connecting from a browser, include the `Sec-WebSocket-Protocol` header as part of the WebSocket handshake.

Set the header value as a single comma-separated string. The order must be:

* Subprotocol name
* Ambient session ID - Your ambient session ID
* Token - Your `sdp_suki_token`

<Note>
  Avoid adding spaces between values unless your client library requires it.
</Note>

For example:

```bash theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
Sec-WebSocket-Protocol: SukiAmbientAuth,<ambient_session_id>,<sdp_suki_token>
```

The server negotiates this subprotocol to establish the connection.

## Non-browser clients

For non-browser clients such as mobile apps, backend services, or testing tools, pass authentication details as separate HTTP headers in the WebSocket upgrade request. Do not use the `Sec-WebSocket-Protocol` header.

Include the following headers:

* `sdp_suki_token` - Session token from login.
* `sdp_provider_id` - Provider identifier. Optional for standard partners; **Required** for Single Auth Token authentication.
* `ambient_session_id` - The ID for the current <Tooltip tip="A single, time-bound instance of an ambient recording for a specific patient encounter that captures clinical conversations." cta="View in Glossary" href="/Glossary/a">ambient session</Tooltip>.

<Warning>
  If you push non-JSON payloads where the server expects JSON, you can see parse errors (for example invalid character or null byte errors).
</Warning>

## Code examples

<Tabs>
  <Tab title="Python">
    ```python theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    # pip install websocket-client
    import base64
    import json
    from datetime import datetime, timezone

    import websocket

    ambient_session_id = "<ambient_session_id>"
    token = "<sdp_suki_token>"
    sdp_provider_id = "<sdp_provider_id>"  # Optional; Required for Single Auth Token authentication
    CHUNK_BYTES = 3200
    WAV_HEADER_BYTES = 44

    # Non-browser: headers on upgrade. Staging WebSocket URL:
    ws_url = "wss://sdp.suki-stage.com/ws/stream"
    ws = websocket.create_connection(
        ws_url,
        header=[
            f"sdp_suki_token: {token}",
            f"sdp_provider_id: {sdp_provider_id}",
            f"ambient_session_id: {ambient_session_id}",
        ],
    )


    def b64(data: bytes) -> str:
        return base64.b64encode(data).decode("ascii")


    try:
        rfc3339 = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        ws.send(json.dumps({"type": "START_TIME", "data": b64(rfc3339.encode("utf-8"))}))

        with open("audio.wav", "rb") as f:
            raw = f.read()
        if len(raw) >= 12 and raw[:4] == b"RIFF" and raw[8:12] == b"WAVE":
            pcm = raw[WAV_HEADER_BYTES:]
        else:
            pcm = raw

        for i in range(0, len(pcm), CHUNK_BYTES):
            chunk = pcm[i : i + CHUNK_BYTES]
            ws.send(json.dumps({"type": "AUDIO", "data": b64(chunk)}))

        ws.send(json.dumps({"type": "AUDIO", "data": b64(b"EOF")}))

        while True:
            try:
                print(ws.recv())
            except websocket.WebSocketConnectionClosedException:
                break
    finally:
        ws.close()
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    // Browser example: same IDs as Python tab; add <input type="file" id="audioFile" /> in your HTML.

    const ambientSessionId = '<ambient_session_id>';
    const token = '<sdp_suki_token>';
    const CHUNK_BYTES = 3200;
    const WAV_HEADER_BYTES = 44;

    function utf8ToBase64(s: string): string {
      const bytes = new TextEncoder().encode(s);
      let binary = '';
      for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!);
      return btoa(binary);
    }

    function bytesToBase64(u8: Uint8Array): string {
      let binary = '';
      for (let i = 0; i < u8.length; i++) binary += String.fromCharCode(u8[i]!);
      return btoa(binary);
    }

    // Staging WebSocket URL
    const ws = new WebSocket(`wss://sdp.suki-stage.com/ws/stream`, [
      `SukiAmbientAuth,${ambientSessionId},${token}`,
    ]);

    ws.binaryType = 'arraybuffer';

    ws.onopen = () => {
      const rfc3339 = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
      ws.send(JSON.stringify({ type: 'START_TIME', data: utf8ToBase64(rfc3339) }));

      const input = document.getElementById('audioFile') as HTMLInputElement;
      const file = input.files?.[0];
      if (!file) return;

      const reader = new FileReader();
      reader.onload = () => {
        const buf = new Uint8Array(reader.result as ArrayBuffer);
        let pcm = buf;
        if (
          buf.length >= 12 &&
          buf[0] === 0x52 &&
          buf[1] === 0x49 &&
          buf[2] === 0x46 &&
          buf[3] === 0x46 &&
          buf[8] === 0x57 &&
          buf[9] === 0x41 &&
          buf[10] === 0x56 &&
          buf[11] === 0x45
        ) {
          pcm = buf.subarray(WAV_HEADER_BYTES);
        }
        for (let i = 0; i < pcm.length; i += CHUNK_BYTES) {
          const chunk = pcm.subarray(i, i + CHUNK_BYTES);
          ws.send(JSON.stringify({ type: 'AUDIO', data: bytesToBase64(chunk) }));
        }
        ws.send(
          JSON.stringify({
            type: 'AUDIO',
            data: bytesToBase64(new Uint8Array([0x45, 0x4f, 0x46])),
          }),
        );
      };
      reader.readAsArrayBuffer(file);
    };

    ws.onmessage = (ev) => console.log('Received:', ev.data);
    ws.onerror = (e) => console.error(e);
    ws.onclose = () => console.log('closed');
    ```
  </Tab>
</Tabs>


## OpenAPI

````yaml GET /ws/stream
openapi: 3.0.1
info:
  title: Suki Developer Platform
  description: >-
    REST and WebSocket APIs for the Suki Developer Platform. Authenticate with
    Login or Register to obtain a Suki access token, then integrate ambient
    clinical documentation, form filling, transcription, and reference metadata
    endpoints.
  contact: {}
  version: '1.0'
servers:
  - url: https://sdp.suki.ai
    description: >-
      Production base URL for Suki Developer Platform REST APIs. WebSocket
      endpoints use the same host with `wss://`.
security:
  - SukiTokenAuth: []
paths:
  /ws/stream:
    get:
      tags:
        - /ws
      summary: Stream ambient audio
      description: >-
        Opens a WebSocket to stream audio for ambient or form-filling sessions.
        Create the session and seed context before connecting. Browser clients
        authenticate with `Sec-WebSocket-Protocol`; non-browser clients send
        `sdp_suki_token`, optional `sdp_provider_id`, and `ambient_session_id`
        as headers.
      parameters:
        - name: Sec-WebSocket-Protocol
          in: header
          description: >-
            Required FOR BROWSER CLIENTS ONLY. Sent during WebSocket handshake.
            Format: 'SukiAmbientAuth,<ambient_session_id>,<sdp_suki_token>'
            (comma-separated, no spaces required between parts).
          schema:
            type: string
        - name: ambient_session_id
          in: header
          description: >-
            Required for non-browser clients only. Session UUID from Create
            Ambient Session or Create Form Filling Session.
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/ProviderIdHeader'
      responses:
        '101':
          description: Switching Protocols - Indicates successful WebSocket handshake.
          content:
            application/json:
              schema:
                type: string
        '200':
          description: >-
            OK - May indicate successful stream processing or completion (e.g.,
            sending 'EOF' message). Specific meaning depends on implementation
            after handshake.
          content:
            application/json:
              schema:
                type: string
        '400':
          description: Bad Request - Invalid parameters, headers, or request format.
          content:
            application/json:
              schema:
                type: string
        '401':
          description: >-
            Unauthorized - Authentication failed (invalid token/session,
            incorrect protocol/headers).
          content:
            application/json:
              schema:
                type: string
        '403':
          description: >-
            Forbidden - Client is authenticated but not authorized for this
            action.
          content:
            application/json:
              schema:
                type: string
        '500':
          description: Internal Server Error - Abnormal closure or server-side issue.
          content:
            application/json:
              schema:
                type: string
      security:
        - SukiTokenAuth: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |-
            curl --request GET \
              --url https://sdp.suki.ai/ws/stream \
              --header 'sdp_suki_token: <sdp_suki_token>' \
              --header 'sdp_provider_id: <sdp_provider_id>' \
              --header 'ambient_session_id: <ambient_session_id>'
components:
  parameters:
    ProviderIdHeader:
      name: sdp_provider_id
      in: header
      description: >-
        **Optional** - Stable identifier for the active provider. Omit for
        standard partners whose `partner_token` identifies the user.
        **Required** for Bearer partners and Single Auth Token authentication
        where multiple providers share one `partner_token`. Use the same
        `provider_id` you sent on Login or Register.
      required: false
      schema:
        type: string
        example: provider-123
  securitySchemes:
    SukiTokenAuth:
      type: apiKey
      in: header
      name: sdp_suki_token
      description: >-
        Suki access token for the authenticated provider. Obtain this by calling
        Login or Register with a valid `partner_token`. Pass the `suki_token`
        value from the JSON response as the `sdp_suki_token` header on REST
        requests and non-browser WebSocket upgrades. Browser WebSocket clients
        pass the token in `Sec-WebSocket-Protocol` instead. Tokens expire after
        one hour; call Login again to refresh.

````