Realtime API reference
This endpoint is a long-lived WebSocket connection. The gateway relays the Bailian Qwen-Omni-Realtime protocol verbatim in both directions with no protocol translation; the full field definitions and the tool-calling (Function Calling / MCP) events follow Bailian's official client events and server events. This page covers the handshake, auth, event list, and audio conventions needed to integrate.
Request
| Item | Value |
|---|---|
| Method | GET (WebSocket upgrade) |
| Path | /api-ws/v1/realtime |
| Full URL | {{ ws_endpoint }}/api-ws/v1/realtime?model=<logical model> |
| Query | model (required) — the platform logical model, not the upstream model |
| Header | Authorization: Bearer <mgk_ API Key> (required) |
| Header | X-Request-ID (optional) — propagated to the call log; the request_id starts with gw_rt_ |
Only a logical model whose public_protocols includes dashscope_realtime can use this surface.
Handshake errors
Failures before the WebSocket upgrade are returned as HTTP status codes with a body of:
{"error":{"type":"invalid_request_error","code":"unauthorized","message":"unauthorized: api_key_not_found"}}| HTTP | code | Trigger | Example message |
|---|---|---|---|
| 401 | unauthenticated | no API key | missing api key |
| 401 | unauthorized | invalid / disabled key | unauthorized: api_key_not_found |
| 404 | model_not_found | model not in catalog | unknown model: <model> |
| 403 | model_protocol_not_offered | model does not advertise dashscope_realtime | model <model> is not offered on the realtime protocol |
| 404 | no_route | no route for the model in the policy (including its fallback chain) | no route for model <model> |
| 503 | no_provider | a route exists but no provider instance is available | no provider available for <model> |
| 402 | insufficient_funds / budget_exceeded | preauthorization failed (balance / budget) | billing: insufficient funds |
After a successful upgrade (HTTP 101), later problems are delivered as an in-band error event, then the connection closes:
{"type":"error","error":{"type":"upstream_error","code":"provider_unavailable","message":"upstream connection failed"}}Session configuration session.update
After connecting, the server first sends session.created; the client overrides with session.update, and a valid update is acknowledged with session.updated. Common fields:
| Field | Values | Default |
|---|---|---|
modalities | ["text","audio"] / ["text"] | ["text","audio"] |
voice / audio.output.voice | see Bailian's voice list | Tina |
turn_detection | {"type":"server_vad"} / {"type":"semantic_vad"} / null | server VAD |
audio.input.format | {"type":"pcm","sample_rate":16000} | pcm / 16000 |
audio.output.format | {"type":"pcm","sample_rate":24000} | pcm / 24000 |
instructions | any text | empty |
input_audio_transcription | {"model":"qwen3-asr-flash-realtime"} / null | enabled |
turn_detection: null disables the server VAD so the client commits and triggers responses manually.
Client events
| Event | Purpose |
|---|---|
session.update | Update session config (modalities, voice, audio format, VAD, instructions, …) |
input_audio_buffer.append | Append Base64 audio to the input buffer |
input_image_buffer.append | Append a Base64 image (JPG/JPEG; audio must have been sent first) |
input_audio_buffer.commit | Commit the audio buffer in manual mode (does not trigger a response) |
input_audio_buffer.clear | Clear the audio buffer |
conversation.item.create | Create an item: plain text input, function_call_output, mcp_approval_response |
response.create | Trigger one model response |
response.cancel | Cancel the in-progress response (interruption) |
Server events
| Event | Meaning |
|---|---|
session.created / session.updated | Session created / config applied |
input_audio_buffer.speech_started / speech_stopped | VAD detected the user starting / stopping (interruption trigger) |
input_audio_buffer.committed / cleared | Audio buffer committed / cleared |
conversation.item.created | An item was created |
conversation.item.input_audio_transcription.delta / .completed | Input speech transcription (streaming preview / final) |
response.created | A response turn began |
response.output_item.added / response.content_part.added | Output item / content part began |
response.text.delta / response.text.done | Text-only streaming / full text |
response.audio_transcript.delta / .done | Transcript of the audio output |
response.audio.delta / response.audio.done | Base64 output audio (24 kHz PCM) / end of audio |
response.output_item.done / response.content_part.done | Output item / content part completed |
response.done | Response turn ended; response.usage carries per-modality token usage |
Tool-calling events (response.function_call_arguments.delta / .done) and the MCP events (mcp_*) are documented in Bailian's server-events reference.
Example event order for one voice turn:
session.created
session.updated
input_audio_buffer.speech_started
input_audio_buffer.speech_stopped
input_audio_buffer.committed
conversation.item.input_audio_transcription.completed
response.created
response.output_item.added
response.audio_transcript.delta …
response.audio.delta …
response.doneAudio formats
| Direction | Format | Field |
|---|---|---|
| Uplink (input) | 16000 Hz, 16-bit little-endian, mono PCM, Base64 | audio of input_audio_buffer.append |
| Downlink (output) | 24000 Hz, 16-bit little-endian, mono PCM, Base64 | delta of response.audio.delta |
Send input audio in full 100 ms frames (3200 bytes per frame) for best reliability. input_audio_buffer.append only buffers; whether it is committed is decided by VAD or by a client input_audio_buffer.commit.
VAD and interruption
- With a non-
nullturn_detection, the server auto-detects speech boundaries and, aftersilence_duration_msof silence (default 800 ms), auto-commits and triggers a response. - When the user speaks again: the server emits
input_audio_buffer.speech_started→ the client stops playback and sendsresponse.cancel→response.done(cancelled) arrives → a new turn begins. turn_detection: nullis manual mode: after recording, the client sendsinput_audio_buffer.commit(wait forinput_audio_buffer.committed) thenresponse.create.
Usage and billing
Each turn's response.done.response.usage carries per-modality tokens:
{
"type": "response.done",
"response": {
"status": "completed",
"usage": {
"total_tokens": 843,
"input_tokens": 693,
"output_tokens": 150,
"input_tokens_details": { "text_tokens": 693 },
"output_tokens_details": { "text_tokens": 28, "audio_tokens": 122 }
}
}
}The gateway settles the accumulated session usage at close: a session-level estimate is held at connect (currently about ¥1.2; insufficient balance fails the handshake with 402), then refund/top-up applies based on actual usage; a session with no usage releases the hold. See the guide.
Limits
| Item | Value |
|---|---|
| Max single connection | 30 minutes (gateway-enforced) |
| Total input tokens | 196,608 |
| Max output tokens | 65,536 |
| Upstream rate limit | 60 RPM |