session.update fields, error and close codes, and limits, see the Dialog-RSN-1 API reference and limits and pricing.
Connecting
session.created with the effective (default) session. You don’t need to send anything to open the conversation.
No SDK required. This is plain JSON over a plain WebSocket. Any WebSocket client in any language can speak this protocol, no special SDK or realtime-specific library needed.
The session object
Sent insidesession.update, echoed back on session.created / session.updated. A partial update deep-merges onto the current session: updating instructions does not wipe tools.
session.update
Client → server events
The server recognizes all eleven GA client event types, plus the three playback reports below. Two of the GA eleven behave differently:conversation.item.truncate only cuts a reply the server spoke, and output_audio_buffer.clear returns invalid_request_error, since it exists only over WebRTC.
Playback reports
Extension. Three events with no GA equivalent, because there is nothing to be equivalent to: OpenAI’s server owns audio output and already knows what a caller is hearing. When you bring your own text-to-speech, only you know. On a session the server speaks, it tracks playback itself and ignores these reports. Event extensions are namespacedpoly., the way field extensions are namespaced poly_.
Sending these makes the barge-in confirmation window follow your real playback. See Turn-taking & audio. They also set history: after
poly.output_audio.stopped, the reply is kept as spoken_text, ending in a dash if dropped_ms says it was cut off. Each report applies to the response it names, so carry the response_id from response.created through every report about it.
Optional. A client that sends none of these gets an estimate instead: the server treats a reply as audible for as long as it takes to say at a typical speaking rate, and after a barge-in keeps the words it estimates were heard. The first report you send replaces the estimate for the rest of the session. On a reply the server speaks itself, it tracks playback and ignores these reports.
Server → client events
Every event this service can send. Session
Conversation items
Audio input / voice activity detection
Transcription
Response lifecycle
Errors
Turn-taking and audio
Voice activity detection proposes a boundary, and Dialog-RSN-1 confirms the end of every turn beforespeech_stopped reaches you.
Detection runs in-process over every appended frame. Crossing into speech opens a turn and mints its item id up front, since speech_started has to name an item before the audio finishes arriving.
Crossing back out of speech doesn’t commit the turn by itself: it triggers a turn-status check, folded into the same generation that becomes the reply if Dialog-RSN-1 confirms the caller is done. If Dialog-RSN-1 says the turn is still ongoing, the buffer keeps growing and the check re-runs on a timer, capped after a few seconds, rather than sending anything to you.
speech_stopped reports that confirmed judgment, never the detector’s raw signal, so a mid-sentence pause that the detector reads as silence doesn’t surface as a turn boundary if Dialog-RSN-1 says the caller isn’t finished. speech_started is the detector’s own signal while nothing is playing, and is confirmed by the model first only while a reply is audible. A turn the model then judges to be noise is dropped, so a speech_started can arrive with no speech_stopped after it.
Setting turn_detection.create_response: false skips the confirmation. Turns are taken at face value: the turn commits at the first pause the detector hears, or when you send input_audio_buffer.commit, and nothing answers it until an explicit response.create. A pause long enough for the detector splits the caller’s speech into two items in history.
The detector’s thresholds are fixed at build time. Tune turn-taking behavior in instructions
instead, which is where this service puts that decision.
Barge-in cancels the in-flight response. If the caller starts talking again while a response is still streaming, Dialog-RSN-1 confirms it’s the caller before cancelling that response (the same as if the client had sent response.cancel), and the usual response.done(status: "cancelled") follows. There’s no separate signal for this; watch for response.done with a cancelled status arriving unprompted, right after a speech_started you didn’t ask for.
That confirmation runs for as long as the reply is audible, and the server’s idea of “audible” is only as good as what you tell it. On its own it’s an estimate: how long the reply takes to say at a typical speaking rate. If your text-to-speech runs slower or starts late, sound arriving after the estimate ends is forwarded without confirmation. Sending playback reports replaces the estimate with your real playback, so a backchannel two seconds into a spoken reply gets checked like any other interruption. When the server speaks the reply, it knows where playback is and needs no reports.
Voice input and transcription
Dialog-RSN-1 transcribes spoken audio as part of the same generation that answers the turn. When a turn’s last item is user audio, the model both answers the turn and produces its transcript in one completion, streamed to the client asconversation.item.input_audio_transcription.delta/.completed, attributed back to the audio item’s id.
A timing note. The transcript is a by-product of the same generation that answers the turn, not an independent asynchronous pass. Don’t rely on the transcript arriving before the response text starts.
Text or spoken replies
A new session speaks its replies. Dialog-RSN-1 writes each reply and the server voices it, so you choose whether to take the speech or the text.output_modalities: ["audio"]: the default. The server speaks the reply. The sound streams asresponse.output_audio.delta, base64 PCM16 mono at 24 kHz, sent at the pace it plays. The words stream asresponse.output_audio_transcript.delta. Pick the voice withaudio.output.voice, a Cartesia voice id; leave it unset for the default voice.response.donearrives once the reply has finished playing.output_modalities: ["text"]: bring your own text-to-speech. The reply streams asresponse.output_text.delta, with no audio events, and you speak it however you like. Use it when you want your own voice or vendor, control over playback, or a place to log the reply and run your guardrails before a caller hears it. Then send playback reports, so barge-in and history follow what the caller really heard.
response.output_text.delta, so a client that reads only text deltas sees an empty reply and no error. Read the transcript deltas, or set ["text"]. Asking for both modalities at once is refused with invalid_request_error.
Function calling
Tool calls stream the same way text does: incremental arguments, then a.done naming the call.
Declare tools in session.tools. When the model calls one, arguments stream via response.function_call_arguments.delta, closing with .done carrying name, full arguments, and a server-minted call_id. Answer it with conversation.item.create (type: "function_call_output", matching call_id), then a fresh response.create. A tool result carries no audio, so nothing else would trigger the follow-up turn.
Example, for a tool declared as get_weather(location: string):
round trip
Errors
Almost every failure answers with anerror event and keeps the connection open. One malformed frame has not earned a disconnect. The exceptions, such as an oversized frame or a session that fails to start, close the socket with a code listed in the API reference, which also has the full code list with retry advice.
Known limitations
These are deliberate scope cuts.- Truncation of a text reply is acknowledged, not applied. On a reply you voice yourself,
conversation.item.truncatevalidates the item and replies, but leaves history untouched: mapping a millisecond offset onto a character offset needs TTS timing the protocol has nowhere to carry. Send playback reports instead, which set history to the words heard. - Without playback reports, barge-in timing is an estimate. Dialog-RSN-1’s barge-in check runs while a reply is audible, and without reports the server estimates that from a typical speaking rate. If your text-to-speech runs behind that estimate, sound arriving after it ends is forwarded as an interruption without confirmation. Playback reports close the gap.
- No MCP tools. Only function tool entries are honored; an MCP tool declaration is rejected with
unsupported_parameter. - Turn timing isn’t configurable. The detector’s thresholds are fixed.
silence_duration_ms,prefix_padding_ms,threshold,eagernessand"type": "semantic_vad"are accepted and ignored;idle_timeout_msandinterrupt_response: falseare rejected withunsupported_parameter. Onlycreate_responseis read. See Turn-taking & audio. - Transcription usage is a duration, not a token count.
conversation.item.input_audio_transcription.completedreports audio duration. Response token counts are separate, and do arrive onresponse.usage. - History is bounded. From the tenth caller audio turn, older turns release their audio and keep their transcript. Where older history is summarised or dropped to fit the context window, the first
response.doneafter that lists the items the prompt now leaves out inpoly_compacted_item_ids; the items stay in history. A conversation that still outgrows the window fails that response withcontext_length_exceeded; the connection stays open. - One response in flight per connection. A
response.createwhile a response is open is refused withconversation_already_has_active_response. A confirmed barge-in cancels the open response. - Without playback reports, history holds an estimate of what was heard. After a barge-in the server keeps the words it estimates the caller heard. Send
poly.output_audio.stoppedwithspoken_textto replace the estimate with your synthesizer’s account. - No session resumption. A dropped connection has no resumption handle to pick back up with, unlike OpenAI’s or Gemini’s resumption tokens. Reconnect and resend whatever context the new session needs.
Example flows
Three complete conversations, shown asdialog-rsn-1 returns them. Flow 1 uses the default spoken output; flows 2 and 3 set output_modalities: ["text"], so the reply arrives as output_text.delta.
1. A plain question, driven manually
Session configured withcreate_response: false, so nothing answers until asked.
- Client → server:
conversation.item.create, a real user message: “What’s the capital of France? Answer in one short sentence.” - Server → client:
conversation.item.added→.done - Client → server:
response.create - Server → client:
response.created→output_item.added→content_part.added→output_audio_transcript.delta×N →output_audio.delta×N →output_audio.done→output_audio_transcript.done→content_part.done→output_item.done→response.done
dialog-rsn-1: “The capital of France is Paris.”
2. Tool calling, end to end
- Client → server:
session.updatewithtools: [get_time, get_weather] - Client → server:
response.create, off “What time is it right now?” - Server → client:
response.function_call_arguments.done,get_time({})→call_000a14b52c2c4c9dae5a307369fad224 - Client → server:
conversation.item.create(function_call_output, mocked result{"time": "11:04 AM", "timezone": "CDT"}) +response.create - Server → client:
response.output_text.delta→response.done
dialog-rsn-1: “It’s 11:04 AM.”
3. Cancellation mid-stream
- Client → server:
response.create - Server → client:
response.created→ a fewoutput_text.delta - Client → server:
response.cancel - Server → client:
response.done, status"cancelled". Any open output item closes asincomplete, so a client waiting on a matching.doneisn’t left hanging.
Next steps
Quickstart
A walkthrough of how the connection, session, and a turn of conversation fit together.
Overview
What realtime streaming is, and how turn-taking, generation, and prompting fit together.
Guides
Build a voice agent from scratch, or on top of a framework you already use.
API reference
Hosts, the session contract, events, PolyAI extensions and error codes.

