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

# Form Filling Audio Streaming

> WebSocket endpoint for streaming visit audio during Form filling sessions

Use this API to stream visit audio over the Partner WebSocket (`GET /ws/stream`) for Form filling sessions. For more information on how to handle the handshake, wire format, message order, and error handling,
and poll for structured data when the session is complete, refer to the [Aaudio streaming](/documentation/ambient-audio-streaming) guide.

<Note>
  Use the `ambient_session_id` from [Create Form Filling session](/form-filling-api-reference/form-filling-sessions/create) while streaming audio.
  Do not use the `ambient_session_id` from an [Create Ambient session](/api-reference/ambient-sessions/create), even though both fields use the name `ambient_session_id`.
</Note>

## Prerequisites

Complete these steps before you open the WebSocket.

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

* **Authenticate** and obtain `sdp_suki_token`
* **Create a Form filling session** with <Badge color="blue" size="sm">POST</Badge> [`/api/v1/form-filling/session/create`](/form-filling-api-reference/form-filling-sessions/create). Save the `ambient_session_id` from the **201 Created** response
* **Seed session context** (recommended) with <Badge color="blue" size="sm">POST</Badge> [`/api/v1/form-filling/session/{ambient_session_id}/context`](/form-filling-api-reference/form-filling-sessions/context). Include `form_template_id` values when you send context
* **Open the WebSocket** at `wss://sdp.suki-stage.com/ws/stream` (staging) or `wss://sdp.suki.ai/ws/stream` (production). Use your Form filling `ambient_session_id` in the handshake

## Browser clients

Send `Sec-WebSocket-Protocol` during the handshake as one comma-separated string, in this order:

* Subprotocol name
* Form filling session ID (`ambient_session_id`)
* `sdp_suki_token`

<Note>
  Do not add spaces between values unless your client library requires them.
</Note>

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 mobile apps, backend services, or testing tools, pass headers on the WebSocket upgrade request. Do not use `Sec-WebSocket-Protocol`.

* `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` - Form filling session ID from [Create Form Filling session](/form-filling-api-reference/form-filling-sessions/create)

<Warning>
  Sending non-JSON payloads where the server expects JSON can cause 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

    # Form filling ambient_session_id from POST /api/v1/form-filling/session/create
    form_filling_session_id = "<form_filling_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

    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: {form_filling_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: use Form filling ambient_session_id from POST /api/v1/form-filling/session/create

    const formFillingSessionId = '<form_filling_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);
    }

    const ws = new WebSocket(`wss://sdp.suki-stage.com/ws/stream`, [
      `SukiAmbientAuth,${formFillingSessionId},${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.

````