dialog-rsn-1.
Limits and pricing
Concurrency, session duration, context window, audio formats and the connected-minute rate.
Dialog-RSN-1 docs
The overview, quickstart, prompt guide, cookbook and integration guides.
OpenAI Realtime reference
Field-level documentation for every event. This page lists what we accept and where we differ.
Connect
Connect towss://api.studio.poly.ai/v1/realtime with an API key from studio.poly.ai. Sign up or log in there and create the key in the API Keys tab. The quickstart has the steps.
Enterprise accounts on
studio.us.poly.ai connect to wss://api.us.poly.ai/v1/realtime instead, with a key from that Studio. A key only works on the host that matches the Studio that issued it, and a mismatch returns 401. Dialog-RSN-1 runs in the US only./realtime to the base URL and sends your key as a bearer token.
session_id ([A-Za-z0-9_.-], up to 128 characters) names the session for your own logs. It comes back as session.poly_session_id. The model query parameter that OpenAI clients send is accepted and ignored.
The server pings every 20 seconds, which keeps a quiet connection open through the gateway. A connection whose client sends no event for 5 minutes is closed with code 4408; a client that is streaming audio never meets this.
Authenticate
Send your API key from studio.poly.ai in either header. They are equivalent.websocket.http.response extension, which can’t answer the handshake with a status. A client library that can’t read the HTTP response sees a failed handshake, not a close code.
Configure the session
Sendsession.update after session.created. Updates are merged into the session, not replaced. A rejected update applies nothing and sends no session.updated, so what comes back in session.updated is exactly what is in effect.
session.update
Honoured
Accepted and ignored
Fields that stock clients send by default and that would change nothing a caller notices are accepted. Most are left out ofsession.updated; audio.input.transcription and parallel_tool_calls are echoed back but change nothing:
model(the session staysdialog-rsn-1),tracing,truncation,includeaudio.input.noise_reduction;audio.input.transcription(transcripts are always sent)- Turn-detection timing:
threshold,silence_duration_ms,prefix_padding_ms,eagerness.type: "semantic_vad"is reported back asserver_vad. The server finds the end of the turn: a voice-activity detector proposes a boundary and the model confirms it. turn_detection.interrupt_response: true, which is already how the service behavesaudio.output.voiceset to an OpenAI voice name (alloy,marinand the rest): the deployment’s default voice speaks instead.audio.output.speed.parallel_tool_calls(the model makes one call per turn)
Refused
Fields that would change what you get if silently ignored are refused with anerror naming the field in param. The whole update is dropped.
Audio in
Audio travels as base64 insideinput_audio_buffer.append. Binary WebSocket frames are refused.
- Send about 20 ms per frame (640 bytes at 16 kHz, 960 at 24 kHz). One frame may not exceed 256 KiB; a larger frame closes the connection with code
1009. - Keep streaming during silence. The server decides where the turn ends from the audio; a client that stops sending looks idle, not quiet.
- One turn may hold up to 300 seconds of audio; anything longer is dropped.
- Audio in
conversation.item.createis refused withunsupported_item_type. Stream caller audio; useconversation.item.createfor text, tool results and history.
Run a turn
Withcreate_response: true the sequence for one caller turn is:
- You stream
input_audio_buffer.appendframes. input_audio_buffer.speech_started, theninput_audio_buffer.speech_stoppedandinput_audio_buffer.committedwhen the model confirms the turn is over.conversation.item.addedandconversation.item.donefor the caller’s turn.response.created,response.output_item.added,response.content_part.added, thenresponse.output_text.deltaframes untilresponse.output_text.done,response.content_part.done,response.output_item.doneandresponse.done.conversation.item.input_audio_transcription.deltaand.completed, carrying what the model heard, arrive during the reply, usually after its first text delta.
create_response: false, the model doesn’t confirm the turn: step 2 follows the first pause the detector hears, or your input_audio_buffer.commit. Step 4 starts when you send response.create.
The samples below set ["text"] so they can print the reply. Leave it out and the API speaks the reply instead: read response.output_audio_transcript.delta for the text and response.output_audio.delta for the audio.
- One response at a time. A
response.createwhile a response is open is refused withconversation_already_has_active_response. Sendresponse.cancelfirst. Two requests are held rather than refused: one sent while the caller is still speaking is answered by the response to their turn, and one sent while a reply the server speaks is still playing is answered when that audio ends. Only one request is held at a time. response.createmay setinstructions,max_output_tokensandmetadata.parallel_tool_callsis ignored. Every other field must match the session or is refused withunsupported_parameter, namingresponse.<field>. That includesconversation: "none"(no out-of-band responses), a per-responseoutput_modalities,toolsortool_choice,input,audio,promptandreasoning. Change the session withsession.updateinstead.- A confirmed barge-in always cancels the reply.
interrupt_responsecannot be turned off. The cancelled response ends with statuscancelled. - Tell the server what the caller heard. If you speak the text through your own TTS, send the
poly.output_audio.*events below so that an interrupted reply is kept in history as the words that were actually played.
Events
Every event is a JSON text frame with atype and an event_id. Fields follow the OpenAI Realtime reference; only the differences are listed here.
Client to server
Server to client
session.created, session.updated, conversation.item.added, .done, .deleted, .retrieved, .truncated, conversation.item.input_audio_transcription.delta, .completed, input_audio_buffer.speech_started, .speech_stopped, .committed, .cleared, response.created, response.done, response.output_item.added, .done, response.content_part.added, .done, response.output_text.delta, .done, response.function_call_arguments.delta, .done, error. On a session with output_modalities: ["audio"], response.output_audio_transcript.delta and .done replace response.output_text.*, and response.output_audio.delta and .done carry the speech as base64 PCM16 mono at 24 kHz.
Not sent: rate_limits.updated, conversation.created, MCP events. Token usage arrives on response.done as response.usage.
Differences a stock client notices
- Text and function-argument deltas arrive per model chunk, not per token. The concatenation is the same.
- Transcription deltas are true increments.
previous_item_idis set onconversation.item.*events, not onresponse.output_item.*.speech_started.audio_start_mscounts audio actually received, not wall-clock time.session.createdandresponse.createdomit optional fields that are not in effect (usage,max_output_tokens,metadataand others).
PolyAI extensions
Extensions are additive fields on existing events, all prefixedpoly_. Nothing is sent as a new server event type.
Playback reports. When you voice the reply yourself, send these so the server knows what the caller heard:
delta every 250 ms or so with the cumulative milliseconds played and, in delta, the text spoken since the last report. The server builds what the caller has heard so far from those increments, so it knows where to cut the reply before stopped arrives. On a barge-in, stopped carries what was dropped and the words that were actually spoken, and the reply is kept in history as those words.
A client that sends no reports gets an estimate instead: the server treats a reply as audible for as long as it takes to say at 15 characters a second, and after a barge-in keeps the words it estimates were heard. On a reply the server speaks itself, it tracks playback and ignores these reports.
Errors
code, never on message. error.event_id names the client frame the error is about, where there is one. type is invalid_request_error for something you sent and server_error for something that failed on our side. A server_error that ends a response also arrives on response.done with status failed. There, status_details.error carries the code in both code and type, for example generation_failed.
Close codes
Handling a refused handshake and a timed-out session:

