Building with the OpenAI Realtime API

9 min read Module 6 of 9 Topic 17 of 25

What you'll learn

  • Establish a WebSocket session against wss://api.openai.com/v1/realtime with model gpt-realtime
  • Configure voice, turn detection, and available tools at session creation
  • Stream audio input as deltas and consume audio output deltas for playback
  • Implement a function call round-trip inside an active realtime session
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

The previous lesson laid out the architectural case for native speech-to-speech: fewer hand-offs, better prosody, less transcript-level control. This lesson makes that concrete by building against OpenAI’s Realtime API, the production entry point for gpt-realtime, walking through session setup, bidirectional audio streaming, and function calling inside a single persistent connection, the same three concerns that shaped every cascaded pipeline earlier in this course, now handled by one model instead of three.

Opening the session

The Realtime API is a WebSocket protocol at wss://api.openai.com/v1/realtime, authenticated the same way as other OpenAI API calls, that stays open for the duration of a conversation rather than issuing one request per turn. Everything, audio input, audio output, function calls, and control events, flows as JSON events over that single connection.

import asyncio
import json
import base64
import websockets

REALTIME_URL = "wss://api.openai.com/v1/realtime?model=gpt-realtime"

async def open_session(api_key: str):
    headers = {"Authorization": f"Bearer {api_key}", "OpenAI-Beta": "realtime=v1"}
    ws = await websockets.connect(REALTIME_URL, extra_headers=headers)

    # Configure voice, turn detection, and available tools before
    # streaming any audio. This is the single most important event
    # in the session's lifecycle.
    await ws.send(json.dumps({
        "type": "session.update",
        "session": {
            "modalities": ["audio", "text"],
            "voice": "verse",
            "turn_detection": {
                "type": "server_vad",
                "threshold": 0.5,
                "silence_duration_ms": 500,
            },
            "tools": [
                {
                    "type": "function",
                    "name": "check_order_status",
                    "description": "Look up the current status of a customer order by order ID.",
                    "parameters": {
                        "type": "object",
                        "properties": {"order_id": {"type": "string"}},
                        "required": ["order_id"],
                    },
                }
            ],
        },
    }))
    return ws

session.update is where every product-level decision from earlier lessons gets encoded: which voice the model speaks in, how sensitive server-side voice activity detection is (tuned via threshold and silence_duration_ms, directly relevant to the barge-in behavior from Module 5), and which functions the model is allowed to call mid-conversation. Skipping this step means running on whatever defaults OpenAI has set, which is rarely the right choice for a specific product.

Streaming audio in both directions

Once the session is configured, audio flows as base64-encoded deltas in both directions: the client appends chunks of the user’s microphone audio to an input buffer, and the server emits chunks of generated response audio as they’re produced, mirroring the streaming synthesis pattern from Module 4 but now happening natively inside the model rather than as a separate TTS call.

async def stream_microphone(ws, mic_audio_chunks):
    """Push user audio into the session as it's captured."""
    async for chunk in mic_audio_chunks:
        await ws.send(json.dumps({
            "type": "input_audio_buffer.append",
            "audio": base64.b64encode(chunk).decode("utf-8"),
        }))
    # With server_vad enabled, the API detects turn boundaries itself;
    # committing manually is only needed in manual turn-detection mode.


async def receive_events(ws, playback, tool_registry):
    async for raw in ws:
        event = json.loads(raw)
        etype = event["type"]

        if etype == "response.audio.delta":
            audio_chunk = base64.b64decode(event["delta"])
            await playback.enqueue(audio_chunk)

        elif etype == "response.audio_transcript.delta":
            # Useful for logging/observability even in a native pipeline.
            log.debug("assistant_partial_transcript", text=event["delta"])

        elif etype == "input_audio_buffer.speech_started":
            # The user has started talking -- this is the barge-in signal.
            await playback.cancel_current_response()

        elif etype == "response.function_call_arguments.done":
            await handle_function_call(ws, event, tool_registry)

Notice response.audio_transcript.delta: even though the model reasons over audio natively, the API still emits a text transcript of its own output alongside the audio, which is a meaningful concession to the observability gap discussed in the previous lesson. It’s not a substitute for full cascaded-pipeline transparency (you still can’t inspect an intermediate ASR transcript of the user’s audio the way you can in a cascaded system), but it does give you a loggable record of what the assistant said, which matters for the same compliance and debugging reasons raised earlier in this module.

Function calling inside a live session

Tool use follows a familiar request-execute-respond shape, adapted to a persistent connection instead of a single request-response call. The model signals intent to call a registered function; your application executes the actual logic (hitting a database, calling an internal service); and the result is sent back into the same session so the model can continue speaking with that information incorporated.

async def handle_function_call(ws, event, tool_registry):
    call_id = event["call_id"]
    name = event["name"]
    args = json.loads(event["arguments"])

    fn = tool_registry[name]
    result = await fn(**args)  # e.g. actually query the orders database

    await ws.send(json.dumps({
        "type": "conversation.item.create",
        "item": {
            "type": "function_call_output",
            "call_id": call_id,
            "output": json.dumps(result),
        },
    }))
    # Ask the model to continue generating a response now that it
    # has the function's result available.
    await ws.send(json.dumps({"type": "response.create"}))

The response.create call at the end matters: adding the function output to the conversation doesn’t automatically trigger the model to speak again, it has to be explicitly told to continue. This is a small but easy-to-miss detail that trips up teams porting function-calling patterns from a text-based chat API to the realtime session shape for the first time, since the text API’s request-response cycle makes this step implicit in a way the persistent WebSocket connection does not.

Knowledge Check

3 questions to test your understanding

1 A developer opens a WebSocket to the Realtime API but never sends a session.update event before streaming audio. What happens to voice, turn detection, and available tools?

2 During an active realtime session, the model decides to call a `check_order_status` function the app registered at session start. What's the correct sequence for handling this?

3 An application wants full manual control over exactly when a user's turn ends, rather than relying on the API's built-in silence detection, because its product has unusually long natural pauses mid-sentence. What configuration choice addresses this?

Discussion

Questions and notes from learners on this topic

Loading discussion…

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan