Real-Time Voice with the Realtime API

AIAudioAPIReal-time
Share on LinkedIn

Phone support bots that wait three seconds after you stop talking feel broken. Users expect the rhythm of human conversation—overlap, interruption, quick acknowledgments. OpenAI's Realtime API maintains a persistent WebSocket where audio flows in both directions, processed by a single multimodal model. You build a voice agent without stitching together separate STT, chat, and TTS services.

Session lifecycle

Client                    Realtime API (wss)
  |--- session.update ---->|  (configure voice, tools, VAD)
  |--- input_audio_buffer ->|  (stream PCM16 chunks)
  |<-- response.audio -----|  (stream output audio)
  |--- response.cancel --->|  (on user interrupt)
  |--- session end -------->|

Audio format: 24 kHz PCM16 mono, base64-encoded in JSON events. Chunk size: 20–100 ms of audio per message balances latency and overhead.

Server-side session setup

Never expose your API key in browser JavaScript. Proxy through your backend:

# FastAPI endpoint that upgrades to WebSocket proxy
from fastapi import WebSocket
import websockets
import os

@app.websocket("/voice")
async def voice_proxy(client_ws: WebSocket):
    await client_ws.accept()
    async with websockets.connect(
        "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview",
        extra_headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"},
    ) as api_ws:
        await api_ws.send(json.dumps({
            "type": "session.update",
            "session": {
                "modalities": ["text", "audio"],
                "voice": "alloy",
                "input_audio_format": "pcm16",
                "output_audio_format": "pcm16",
                "turn_detection": {"type": "server_vad"},
                "instructions": "You are a concise support agent for Acme Cloud.",
            },
        }))
        await asyncio.gather(
            relay(client_ws, api_ws),
            relay(api_ws, client_ws),
        )

Client audio capture

const SAMPLE_RATE = 24000;
const ws = new WebSocket("wss://app.example.com/voice");

const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const ctx = new AudioContext({ sampleRate: SAMPLE_RATE });
const source = ctx.createMediaStreamSource(stream);
const processor = ctx.createScriptProcessor(4096, 1, 1);

processor.onaudioprocess = (e) => {
  const pcm = float32ToInt16(e.inputBuffer.getChannelData(0));
  ws.send(JSON.stringify({
    type: "input_audio_buffer.append",
    audio: btoa(String.fromCharCode(...new Uint8Array(pcm.buffer))),
  }));
};
source.connect(processor);
processor.connect(ctx.destination);

Commit audio when VAD detects end of speech, or let server VAD handle turn boundaries automatically.

Function calling in voice

{
  "type": "session.update",
  "session": {
    "tools": [{
      "type": "function",
      "name": "check_order_status",
      "description": "Look up order by ID",
      "parameters": {
        "type": "object",
        "properties": {
          "order_id": { "type": "string" }
        },
        "required": ["order_id"]
      }
    }],
    "tool_choice": "auto"
  }
}

On response.function_call_arguments.done, execute the function and inject the result:

{
  "type": "conversation.item.create",
  "item": {
    "type": "function_call_output",
    "call_id": "call_abc",
    "output": "{\"status\": \"shipped\", \"eta\": \"2025-08-15\"}"
  }
}

Then send response.create to let the model speak the answer.

Interruption handling

When input_audio_buffer.speech_started fires while audio plays:

ws.send(JSON.stringify({ type: "response.cancel" }));
audioQueue.clear();

On the client, maintain a short playback buffer. Canceling within 300 ms of detected speech sounds natural; longer delays feel like the bot is ignoring the user.

Latency budget

Stage Target
Mic capture → WebSocket 20–50 ms
Server VAD end-of-turn 300–500 ms
Model inference + first audio 200–400 ms
Playback start 50 ms
Total perceived ~600–1000 ms

Log response.audio_transcript.done timestamps against input_audio_buffer.speech_stopped to measure your actual turn latency.

Cost and limits

Realtime API bills per audio token (input and output). A 5-minute conversation costs roughly $0.30–$0.60 depending on model. Set max_response_output_tokens to prevent runaway monologues. Monitor session duration and implement idle timeouts (60 seconds of silence closes the session).

WebSocket session management

Production Realtime API requires robust session lifecycle:

class RealtimeSession {
  constructor(apiKey) {
    this.ws = new WebSocket("wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview",
      ["realtime", `openai-insecure-api-key.${apiKey}`]);
    this.audioQueue = [];
    this.isResponding = false;
  }

  configure() {
    this.ws.send(JSON.stringify({
      type: "session.update",
      session: {
        turn_detection: { type: "server_vad", silence_duration_ms: 500 },
        input_audio_format: "pcm16",
        output_audio_format: "pcm16",
        max_response_output_tokens: 512,
        instructions: "You are a helpful voice assistant. Be concise.",
      }
    }));
  }

  sendAudio(pcm16Buffer) {
    this.ws.send(JSON.stringify({
      type: "input_audio_buffer.append",
      audio: btoa(String.fromCharCode(...new Uint8Array(pcm16Buffer))),
    }));
  }
}

Always configure session before sending audio. Server VAD handles turn detection — don't implement client-side VAD unless latency requires it.

Audio format and buffering

Realtime API expects PCM16 at 24kHz mono:

// Capture from microphone
const audioContext = new AudioContext({ sampleRate: 24000 });
const processor = audioContext.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (e) => {
  const pcm16 = float32ToPcm16(e.inputBuffer.getChannelData(0));
  session.sendAudio(pcm16);
};

// Playback: buffer 100ms before starting to avoid underruns
const PLAYBACK_BUFFER_MS = 100;
function onAudioDelta(base64Audio) {
  audioQueue.push(decodePcm16(base64Audio));
  if (!isPlaying && bufferDuration() >= PLAYBACK_BUFFER_MS) startPlayback();
}

Mismatch in sample rate causes pitch/speed distortion. Resample in browser if mic doesn't support 24kHz natively.

Function calling in voice sessions

Extend voice agents with tool use:

// Register tools in session config
session: {
  tools: [{
    type: "function",
    name: "lookup_order",
    description: "Look up order status by order ID",
    parameters: { type: "object", properties: { order_id: { type: "string" } } }
  }]
}

// Handle tool calls
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === "response.function_call_arguments.done") {
    const result = await lookupOrder(msg.arguments.order_id);
    ws.send(JSON.stringify({
      type: "conversation.item.create",
      item: { type: "function_call_output", call_id: msg.call_id, output: JSON.stringify(result) }
    }));
    ws.send(JSON.stringify({ type: "response.create" }));
  }
};

Voice + tools enables transactional voice agents (order lookup, appointment booking) — not just conversational.

Failure modes

Production checklist

Resources

Frequently asked questions

How is the Realtime API different from chaining Whisper and TTS?

The Realtime API processes audio in a single speech-to-speech model over a persistent WebSocket, cutting end-to-end latency to 300–800 ms. A Whisper → LLM → TTS pipeline typically adds 2–5 seconds per turn due to serial API calls and buffering.

How do I handle user interruptions (barge-in)?

Enable server-side voice activity detection or send client-side VAD events. When the user speaks during model output, send response.cancel to stop audio generation immediately. Buffer only 200–300 ms of playback on the client for smooth cutoffs.

Can I use function calling with voice agents?

Yes. Define tools in the session configuration. The model emits function_call events during conversation; execute the tool server-side and return results via conversation.item.create before requesting the next response.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →