Skip to content

Streaming API

WSS wss://medistream-staging.sahl.ai/ws/summary

Use the streaming API to build your own recording experience. Stream consultation audio over one WebSocket and receive a live transcript, then a structured clinical note when the consultation ends.

One WebSocket connection is one consultation.

sequenceDiagram
    autonumber
    participant C as Your client
    participant S as Sahl AI
    C->>S: Connect (subprotocols: listen-protocol, token)
    C->>S: start
    S-->>C: encounter_created
    loop Every ~200 ms
        C->>S: audio_chunk (seq_id n)
        S-->>C: ack (seq_id n)
        S-->>C: transcript_item
    end
    C->>S: end_transcript
    S-->>C: ack (remaining seq_ids)
    S-->>C: task_status (SUCCESS, summary[])
    S--xC: Socket closed by server

Connect

Get a token with username and password or a signed JWT. With a signed JWT, exchange the assertion for a session token first: the WebSocket does not accept the assertion itself. Pass it as the second WebSocket subprotocol, after listen-protocol. Browsers cannot set headers on a WebSocket handshake, so the token travels in the subprotocol list.

const ws = new WebSocket("wss://medistream-staging.sahl.ai/ws/summary", ["listen-protocol", accessToken]);
import WebSocket from "ws";
const ws = new WebSocket("wss://medistream-staging.sahl.ai/ws/summary", ["listen-protocol", accessToken]);
import websockets

async with websockets.connect(
    "wss://medistream-staging.sahl.ai/ws/summary",
    subprotocols=["listen-protocol", access_token],
) as ws:
    ...

If the token is invalid or expired, the server rejects the handshake or closes the socket right after it opens. Refresh the token before a consultation starts, never partway through.

Audio format

Property Value
Encoding Raw PCM, signed 16-bit little-endian. Not WAV, MP3 or WebM.
Sample rate 16,000 Hz
Channels 1 (mono)
Chunk length 100–200 ms per message (3,072 samples ≈ 192 ms works well)
Transport Base64 string inside a JSON text frame

Every message in this protocol is a JSON text frame with an object field. Do not send binary frames.

Converting Web Audio Float32Array samples (already resampled to 16 kHz mono):

function toBase64Pcm16(float32) {
  const pcm = new Int16Array(float32.length);
  for (let i = 0; i < float32.length; i++) {
    const s = Math.max(-1, Math.min(1, float32[i]));
    pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
  }
  const bytes = new Uint8Array(pcm.buffer);
  let bin = "";
  for (let i = 0; i < bytes.length; i += 0x8000) {
    bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
  }
  return btoa(bin);
}

Minimal session

// → connect wss://medistream-staging.sahl.ai/ws/summary  subprotocols ["listen-protocol", "<TOKEN>"]
→ {"object":"start","prompt_id":"<PROMPT_ID>","consultation_name":"Follow-up","show_transcript":true,"ext_patient_id":"MRN-884213","visit_id":"VISIT-2211045"}
← {"object":"encounter_created","encounter_id":"6b0d5c39-1f2a-4e77-b0aa-2c9d4e81f5b3"}
→ {"object":"audio_chunk","payload":"M/8x/y3/…","seq_id":1}
← {"object":"ack","seq_id":1}
← {"object":"transcript_item","id":"…","text":"Good morning, how are you feeling today?","start_offset_ms":0,"end_offset_ms":2600,"timestamp":"2026-11-01T09:14:20.001Z"}
→ {"object":"end_transcript","seq_id":842}
← {"object":"ack","seq_id":842}
← {"object":"task_status","status":"SUCCESS","summary":[{"title":"Chief Complaint","body":["…"],"edited_body":null,"show":true}],"errorMsg":""}
// ← server closes the socket

Next, read the full message reference and the reliability requirements. The reliability rules are mandatory for production clients.