Speech to text in realtime
Stream live audio over a WebSocket and receive each turn's transcript as it closes.
To transcribe a live stream without cutting it into files, open a realtime WebSocket in transcription mode. Text arrives as OpenAI-compatible input-transcription delta and completed events.
Rhythm decodes a turn once the turn closes, so deltas for a turn follow its
input_audio_buffer.speech_stopped rather than arriving mid-utterance. One
session covers a whole conversation, and the server finds the turn boundaries,
which is what this transport buys over one REST request per turn.
Connection
Realtime sessions run over WebSocket only. There is no WebRTC transport, no SDP exchange, and no separate signaling endpoint.
wss://api.sprag.ai/v1/realtime?intent=transcriptionintent=transcription selects transcription over spoken conversation: the model
transcribes and does not answer. For a session that speaks back, see
speech to speech.
Authentication
Server-side clients authenticate the upgrade with a bearer token.
Authorization: Bearer $SPRAG_API_KEYBrowsers cannot set that header on a WebSocket upgrade, so they pass the
credential as a subprotocol pair instead: sprag-jwt with a short-lived Sprag
JWT, or sprag-api-key with an API key. Use the JWT from a browser, and never
ship a long-lived API key in frontend code.
new WebSocket("wss://api.sprag.ai/v1/realtime?intent=transcription", [
"sprag-jwt",
shortLivedSpragJwt,
]);A rejected credential or a rate limit fails the HTTP upgrade with 403 before
the socket opens, so handle those failures as a connection error rather than a
close frame.
Declaring the model
A transcription session starts with no model. Send a session.update naming one
under session.audio.input.transcription.model within ten seconds of
connecting, or the server closes the socket with code 1008. An unrecognized
model id closes it the same way.
Name rhythm or symphony, the models that serve transcription sessions.
Leave session.type out: a transcription session answers "type": "realtime"
with an unsupported_session_type error.
{
"type": "session.update",
"session": {
"audio": {
"input": {
"format": { "type": "audio/pcm", "rate": 16000 },
"transcription": { "model": "rhythm" },
"turn_detection": {
"type": "server_vad",
"threshold": 0.5,
"prefix_padding_ms": 300,
"silence_duration_ms": 500
}
}
}
}
}The server emits session.created on connect and answers each accepted update
with session.updated, whose payload echoes the resolved session.
Audio format
Declare your input format and capture rate in audio.input.format. The session
decodes linear PCM16 only, and a new session starts at audio/pcm and 24000 Hz,
so use the object form {"type": "audio/pcm", "rate": 16000} to state your own
rate. The bare string pcm16 is accepted but leaves the rate where it was.
A declared G.711 codec is refused with unsupported_audio_format, in both the
spellings the server recognizes: the bare strings g711_ulaw and g711_alaw,
and the object types audio/pcmu and audio/pcma. The whole session.update
carrying it is discarded, so transcode to PCM16 before streaming.
Mixing the two spellings, as in {"type": "g711_ulaw"}, produces a format
object the server does not recognize: it is ignored rather than rejected, and
the session keeps the format it already had. Read the session.updated echo to
confirm which format took.
Sending audio
Stream base64-encoded PCM16 through input_audio_buffer.append. Audio travels
inside JSON events; a binary WebSocket frame closes the session with code
1003.
{ "type": "input_audio_buffer.append", "audio": "<base64 pcm16>" }Turn detection
Server VAD closes the user turn when input audio goes quiet for the configured duration. Every sub-field is optional.
| Field | Default | Effect |
|---|---|---|
threshold | 0.5 | Speech-probability floor. Raise it to reject breaths and background noise. |
prefix_padding_ms | 300 | Audio kept ahead of the detected speech start. |
silence_duration_ms | 500 | Quiet time that ends the turn. Raise it to hold mid-sentence pauses inside one turn. |
For manual turns, set turn_detection to null and close each turn with
input_audio_buffer.commit. Do not follow it with response.create: a
transcription session has no reply to generate. A commit holding less than
100 ms of audio is refused with input_audio_buffer_commit_empty, and the audio
it held is discarded rather than kept for a later commit.
Events
| Event | Meaning |
|---|---|
session.created | The session exists, with no model declared yet. |
session.updated | An update was accepted. The payload is the resolved session. |
input_audio_buffer.speech_started, speech_stopped | Server VAD found the edges of a turn. |
input_audio_buffer.committed | A turn closed and is queued for transcription. |
input_audio_buffer.cleared | A pending buffer was discarded. |
conversation.item.added, conversation.item.done | The turn became a conversation item. Its transcript is still null here. |
conversation.item.input_audio_transcription.delta | A fragment of one item's transcript. |
conversation.item.input_audio_transcription.completed | That item's full transcript, plus a usage record carrying its measured duration. |
conversation.item.input_audio_transcription.failed | Transcription failed for that item. |
conversation.item.truncated | An item was cut short. |
error | A rejected event, carrying a code such as unsupported_audio_format. Errors report in band; a close code means the session is gone. |
Deltas and the completed event both key off item_id, so accumulate text per
item rather than per session.
{
"type": "conversation.item.input_audio_transcription.completed",
"item_id": "item_cd99c2b70002",
"content_index": 0,
"transcript": "Thanks, everyone, for joining.",
"usage": { "type": "duration", "seconds": 1.696 }
}A transcription session emits no response.* events, and no event carries word
offsets. For word timings, use REST; see
timestamps.
Limits and close codes
| Limit | Value |
|---|---|
| Concurrent sessions | 25 per user by default |
| Model declaration window | 10 seconds from connect |
| Smallest manual commit | 100 ms of audio |
| Close code | Cause |
|---|---|
1003 | A binary frame arrived. Send audio as base64 inside input_audio_buffer.append. |
1008 | No model was declared inside the ten-second window, or the declared id is unrecognized. |
1011 | Server error. |
1013 | The session pool is full or you are at the concurrency cap. Retry with backoff. |