
Een voice agent bouwen op de OpenAI Realtime API: de 7-stappen productietutorial (2026)
Onze testagent nam een Twilio-oproep aan en sprak zijn eerste woord 1,1 seconde nadat de beller stopte met praten. Dat is de p50 round-trip, gemeten over 40 oproepen met gpt-realtime-2 en semantic_vad. Geen magie. De OpenAI Realtime API doet speech-to-speech in één socket, dus je slaat de STT → LLM → TTS-estafette over die zo'n 600 ms lijmcode toevoegt. Maar met de standaardwaarden kom je niet op een seconde. Dit is de 7-stappen build die we hebben uitgebracht, met de code en de latentietabel.
Dit is een bouwtutorial, geen conceptuitleg. Wil je eerst de gelaagde opsplitsing, lees dan wat een AI-voice-agent eigenlijk is en kom daarna terug. Alles hieronder gaat ervan uit dat je een OpenAI API-sleutel en een Node-runtime hebt.
Belangrijkste punten:
- gpt-realtime-2 doet speech-to-speech in één socket, geen STT/LLM/TTS-estafette, ~600 ms bespaard.
- Genereer vluchtige sleutels aan de serverkant; stuur je standaard API-sleutel nooit naar een browser.
- Twilio's mediastream is 8 kHz μ-law; resample naar 24 kHz PCM16 voor de Realtime API.
- We maten p50 1,1 s / p95 1,9 s round-trip. Barge-in loopt via
response.cancel.
Wat je in 7 stappen bouwt
Deze tutorial bouwt een telefoonbeantwoordende OpenAI Realtime API voice agent die binnen 1,5 seconde terugpraat, midden in het gesprek een echte functie aanroept en de beller laat onderbreken. De flow is kort: een beller belt een nummer, audio streamt naar je server, je server bridget het via één socket naar gpt-realtime-2, het model praat en kan tool-aanroepen afvuren, en audio streamt terug.
Hier is het pad, en je kunt stoppen bij elke stap die past bij je use case:
- Een vluchtige sleutel genereren (server-route)
- De sessie openen en configureren
- Function calling toevoegen
- Bridgen naar een telefoonnummer met Twilio
- Barge-in en onderbrekingen afhandelen
- Latentie afstemmen tot onder de seconde
- Uitrollen en hardenen voor productie
Drie transports dragen de audio, en je keuze hangt af van waar de audio vandaan komt. Een browser legt het direct vast (WebRTC), je server heeft al een ruwe stream (WebSocket), of een telefoonnetwerk levert het (SIP). We gebruiken WebSocket voor de Twilio-brug en noemen de andere waar ze passen.
Stap 1: Een vluchtige sleutel genereren (de route die je niet mag overslaan)
Stel je standaard OpenAI API-sleutel nooit bloot aan een browser of clientapparaat. De Realtime API geeft kortlevende vluchtige sleutels uit precies hiervoor. Je server roept POST /v1/realtime/client_secrets aan met je echte sleutel, geeft de client een token dat in ongeveer een minuut verloopt, en de client verbindt daarmee in plaats daarvan.
Hier is een minimale Express-route die er een genereert:
// server.js
import express from "express";
const app = express();
app.get("/session", async (req, res) => {
const r = await fetch("https://api.openai.com/v1/realtime/client_secrets", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
session: { type: "realtime", model: "gpt-realtime-2" },
}),
});
const data = await r.json();
res.json({ client_secret: data.value, expires_at: data.expires_at });
});
app.listen(3000);De browser haalt /session op, leest het kortlevende secret en opent er de Realtime-verbinding mee. Als je agent alleen serverkant is (het Twilio-geval in stap 4), kun je de client-overdracht overslaan en de socket vanuit je backend rechtstreeks met de standaardsleutel openen. De vluchtige flow bestaat om niet-vertrouwde clients te beschermen.
Stap 2: De sessie openen en gpt-realtime-2 configureren
Open een verbinding en stuur dan een session.update die het model, audioformaat, de stem en de turn-detectie instelt. De OpenAI-documentatie raadt aan te starten met reasoning.effort op low en het alleen te verhogen als je tool-logica meer nauwkeurigheid nodig heeft, want hogere effort kost je latentie. Audio loopt als 24 kHz PCM16 in beide richtingen.
ws.send(JSON.stringify({
type: "session.update",
session: {
type: "realtime",
model: "gpt-realtime-2",
output_modalities: ["audio"],
audio: {
input: { format: "pcm16", sample_rate: 24000 },
output: { format: "pcm16", sample_rate: 24000, voice: "marin" },
},
instructions: "Je bent een reserveringsagent voor een restaurant. Wees kort.",
reasoning: { effort: "low" },
turn_detection: { type: "semantic_vad" },
},
}));Welk transport je om die socket wikkelt, hangt af van de audiobron:
| Transport | Gebruik wanneer | Audiobron |
|---|---|---|
| WebRTC | Een browser of mobiele app legt de microfoon direct vast | Clientapparaat |
| WebSocket | Je server heeft al een ruwe audiostream | Serverpijplijn |
| SIP | Je wilt dat OpenAI het telefoongedeelte afhandelt | PSTN / telefonie |
Voor de volledige veldenlijst van de sessie en de GA-functieset is de OpenAI Realtime API-documentatie de bron van waarheid. We gebruiken WebSocket omdat Twilio ons in stap 4 ruwe audio aanlevert.
Stap 3: Function calling toevoegen (zodat de agent echt iets kan doen)
Een voice agent die niet kan handelen, is een voice-over. Function calling laat gpt-realtime-2 midden in het gesprek pauzeren, je code iets laten uitvoeren en met het resultaat verder praten. Je declareert een tool in de sessie, het model stuurt een function_call_arguments.done-event als het de tool wil, je voert het werk uit en stuurt de output terug.
Declareer de tool en handel daarna het event af:
// in session.update -> session.tools:
tools: [{
type: "function",
name: "book_reservation",
description: "Boekt een tafel voor een groepsgrootte en tijd.",
parameters: {
type: "object",
properties: {
party_size: { type: "integer" },
time: { type: "string", description: "ISO 8601 datum/tijd" },
},
required: ["party_size", "time"],
},
}]
// afhandeling van de aanroep:
if (event.type === "response.function_call_arguments.done") {
const args = JSON.parse(event.arguments);
const result = await bookTable(args); // je echte logica
ws.send(JSON.stringify({
type: "conversation.item.create",
item: {
type: "function_call_output",
call_id: event.call_id,
output: JSON.stringify(result),
},
}));
ws.send(JSON.stringify({ type: "response.create" })); // laat het resultaat uitspreken
}De meest voorkomende reden waarom tools stilletjes nooit afvuren: niet luisteren naar function_call_arguments.done en daarna geen response.create sturen. Het model produceerde de aanroep, jij negeerde hem, de beller hoort radiostilte.
Als je agent met veel tools jongleert, veranderde de OpenAI Agents SDK de rekensom hier. De revisie van 15 april 2026 maakte het Model Context Protocol (MCP) eersteklas en maakte sub-agent-overdrachten een runtime-primitief. Zo kan, in plaats van elke tool in één prompt te proppen, een router-agent een boeking aan een reserverings-sub-agent doorgeven en een factureringsvraag aan een andere. De voice-quickstart van de Agents SDK wikkelt dezelfde Realtime-sessie in een RealtimeAgent en geeft je overdrachten zonder je eigen orkestratielus te schrijven.
Stap 4: Bridgen naar een telefoonnummer (Twilio)
Om echte oproepen aan te nemen, bridge je een telefonie-provider in de socket. Met Twilio richt je een inkomende oproep op een TwiML <Connect><Stream> die een WebSocket naar je server opent, en je relayet audioframes tussen Twilio en de Realtime API. SIP is het alternatief. OpenAI Realtime accepteert SIP rechtstreeks, wat je mediarelay helemaal weghaalt als je de audio niet hoeft aan te raken.
De TwiML die de stream start:
<Response>
<Connect>
<Stream url="wss://your-server.com/twilio-stream" />
</Connect>
</Response>Hier is de valkuil die een dag kost als je hem mist: Twilio's mediastream is 8 kHz μ-law, en de Realtime API wil 24 kHz PCM16. Je resamplet in beide richtingen, of je krijgt vervormde chipmunk-audio.
// inkomend: Twilio (8kHz μ-law base64) -> Realtime (24kHz PCM16)
const pcm16 = upsample(muLawDecode(Buffer.from(msg.media.payload, "base64")), 8000, 24000);
realtime.send(JSON.stringify({
type: "input_audio_buffer.append",
audio: pcm16.toString("base64"),
}));
// uitgaand: Realtime (24kHz PCM16) -> Twilio (8kHz μ-law)
const ulaw = muLawEncode(downsample(modelPcm16, 24000, 8000));
twilioWs.send(JSON.stringify({
event: "media",
media: { payload: ulaw.toString("base64") },
}));Het volledige frameformaat staat in de Twilio Media Streams-documentatie. Houd het resamplen goedkoop, want een zware bibliotheek hier voegt latentie toe die je bij elk frame betaalt.
Stap 5: Barge-in en onderbrekingen afhandelen
Een productieagent laat de beller eroverheen praten. Barge-in betekent detecteren dat de beller begon te praten terwijl de agent midden in een zin zit, en de agent dan netjes afkappen. De Realtime API regelt dit met response.cancel: wanneer turn-detectie meldt dat spraak begon tijdens het afspelen, annuleer je het actieve antwoord en leeg je de al gebufferde audio richting de beller.
if (event.type === "input_audio_buffer.speech_started") {
realtime.send(JSON.stringify({ type: "response.cancel" }));
twilioWs.send(JSON.stringify({ event: "clear" })); // verwijder gebufferde weergave
}Turn-detectie heeft twee modi, en de keuze telt. server_vad triggert op ruwe stiltedrempels en kapt de beller vaak af bij natuurlijke pauzes. semantic_vad wacht tot het model denkt dat de beller echt een gedachte afmaakte, en levert zo veel minder valse onderbrekingen bij een denkpauze. Voor telefoongesprekken is semantische VAD degene die menselijk aanvoelt.
Stap 6: Latentie afstemmen tot onder de seconde
Hier wordt een demo een product, dus hier de cijfers van onze eigen build, geen theoretisch budget. We draaiden dezelfde restaurantagent over 40 testoproepen in mei 2026, op één kleine server gecoloceerd nabij de OpenAI-regio, waarbij we alleen de turn-detectie- en redeneerinstellingen wisselden.
| Configuratie | p50 round-trip | p95 | Opmerkingen |
|---|---|---|---|
| server_vad, reasoning low | ~1,4 s | ~2,3 s | meer valse barge-ins bij pauzes |
| semantic_vad, reasoning low | ~1,1 s | ~1,9 s | onze productiestandaard |
| semantic_vad, reasoning medium | ~1,8 s | ~3,1 s | betere tool-nauwkeurigheid, trager |
De knoppen die echt iets bewogen, op volgorde van impact:
- Houd
reasoning.effortop low tenzij een specifieke tool de nauwkeurigheid echt nodig heeft. Medium verdubbelde onze p50 bijna. - Duw audio niet sneller dan realtime.
input_audio_buffer.appendoverspoelen laat de buffer overlopen en veroorzaakt drift; cadanceer frames op de wandklok. - Houd de socket warm. Per oproep een verbinding koud openen voegt de handshake toe aan je eerste-woord-latentie. Pool verbindingen waar het oproepvolume het toelaat.
- Resample efficiënt. Een naïeve resampler in het hete pad voegde bij ons ~80 ms per beurt toe.
Wat kost het draaien per minuut zodra het live is? We deden de bring-your-own-key-rekensom apart, zie wat een BYOK-voice-agent per minuut kost in plaats van die hier opnieuw af te leiden.
Stap 7: Uitrollen en hardenen voor productie
De kloof tussen "het werkte op mijn laptop" en "het overleeft 500 oproepen per dag" is een handvol bekende faalpunten. Hier de hardening-checklist, getrokken uit de fouten die Realtime-agenten echt breken:
| Valkuil | Symptoom | Oplossing |
|---|---|---|
| Verkeerde sample rate | vervormde / chipmunk-audio | 24 kHz PCM16 beide kanten op |
function_call_arguments.done negeren | tools vuren nooit | luisteren en response.create sturen |
| Audio sneller dan realtime duwen | bufferoverloop, drift | frames op realtime cadanceren |
| Geen reconnect-logica | oproepen vallen weg bij een socket-hapering | auto-reconnect + sessie hervatten |
Geen response.done-afhandeling | overlappende beurten | volgende beurt afhankelijk van response.done |
Nog twee dingen voor echt verkeer. Bij lange oproepen roteer of reseed je de sessie om de paar beurten zodat de context niet driftt, want een gesprek van 20 minuten stapelt staat op waar het model over begint te struikelen. En log elke tool-aanroep met zijn argumenten en resultaat; als een beller zegt "de agent boekte de verkeerde tijd", vertelt het transcript alleen je niet of het model of je code fout zat.
Als je de Agents SDK-route uit stap 3 kiest, draait zijn nieuwe container-sandbox tool-code geïsoleerd, wat belangrijk wordt zodra je tools een bestandssysteem of shell aanraken in plaats van enkel een API.
Wanneer je beter een beheerd platform kunt kopen
Direct op de Realtime API bouwen geeft je de meeste controle en de laagste kosten per minuut, maar jij bezit de reconnect-logica, de telefoonbrug, compliance en observability, alle weinig glamoureuze delen van stap 4 tot 7. Heb je deze week een telefoonagent live nodig en wil je geen mediarelay onderhouden, dan is een beheerd platform de snellere keuze.
We bouwden dezelfde agent op de drie grote en vergeleken ze eerlijk: Retell, Vapi of Bland. Twijfel je nog aan welke kant van de lijn je staat, loop dan het volledige build-vs-buy-beslissingskader door voordat je engineeringtijd vastlegt.
Wanneer teams de controle van een op maat gemaakte Realtime-build willen zonder die te bemannen, is dat het werk dat wij doen: voice-agent-ontwikkeling voor productie, van de telefoonbrug tot de latentie-afstemming hierboven. We kijken graag naar je use case als je hem afweegt.
Over de auteur — Mert Batur is medeoprichter van Techsy.io, waar het team AI-agents, automatiseringssystemen en voice/SDR-pijplijnen levert voor B2B-klanten. Hij schrijft over de LLM-toolingstack die het Techsy-team echt in productie gebruikt. LinkedIn
Veelgestelde vragen
Wat is de latentie van een OpenAI Realtime API voice agent?
In onze build op gpt-realtime-2 met semantic_vad en lage redeneer-effort mat de round-trip-latentie p50 1,1 s en p95 1,9 s over 40 testoproepen. Speech-to-speech in één socket vermijdt de STT/LLM/TTS-estafette, en dat maakt reacties onder de seconde überhaupt mogelijk.
Heb ik WebRTC, WebSocket of SIP nodig voor mijn voice agent?
Gebruik WebRTC wanneer een browser of mobiele app de microfoon direct vastlegt, WebSocket wanneer je server al een ruwe audiostream heeft (het Twilio-bruggeval), en SIP wanneer je wilt dat OpenAI het telefoongedeelte zonder je eigen mediarelay afhandelt. De meeste telefoonagenten gebruiken WebSocket of SIP.
Hoe verbind ik de OpenAI Realtime API met Twilio?
Richt een inkomende Twilio-oproep op een TwiML <Connect><Stream> die een WebSocket naar je server opent, en relay daarna audio tussen Twilio en de Realtime-socket. Resample Twilio's 8 kHz μ-law naar de 24 kHz PCM16 van de API in beide richtingen, anders komt de audio vervormd uit.
Hoe werkt function calling in de Realtime API?
Je declareert tools in de sessieconfiguratie. Wanneer het model er een wil, stuurt het een function_call_arguments.done-event. Je voert het werk uit, stuurt het resultaat terug als function_call_output-conversatie-item, en stuurt dan response.create zodat de agent het resultaat uitspreekt. Die laatste stap vergeten is waarom tools vaak "stilletjes" falen.
Hoe handel je onderbrekingen (barge-in) af in de Realtime API?
Wanneer turn-detectie input_audio_buffer.speech_started meldt tijdens het afspelen, stuur dan response.cancel om het actieve antwoord te stoppen en leeg eventuele gebufferde uitgaande audio richting de beller. Combineer het met semantic_vad zodat natuurlijke pauzes geen valse onderbrekingen midden in een zin triggeren.
Welke audio-sample-rate gebruikt de OpenAI Realtime API?
De Realtime API gebruikt 24 kHz PCM16-audio in beide richtingen. Telefonie-providers zoals Twilio leveren 8 kHz μ-law, dus een telefoonbrug moet bij binnenkomst omhoog en bij uitgang omlaag resamplen. Niet-overeenkomende sample rates zijn de meest voorkomende oorzaak van vervormde audio.
Hoeveel kost het om een voice agent op de Realtime API te draaien?
De kosten worden bepaald door audio-invoer- en uitvoerminuten op gpt-realtime-2, en de bring-your-own-key-economie verschilt sterk van een beheerd per-minuut-platform. We deden de volledige rekensom in onze prijsanalyse van voice agents in plaats van die hier te schatten.
Moet ik op de Realtime API bouwen of Retell, Vapi of Bland gebruiken?
Bouw direct wanneer je maximale controle en de laagste kosten per minuut wilt en reconnects, telefonie en compliance kunt bezitten. Koop een beheerd platform wanneer snelheid naar lancering belangrijker is. Onze Retell vs Vapi vs Bland-vergelijking en het build-vs-buy-kader behandelen de afwegingen.
Wat veranderde de OpenAI Agents SDK-update van april 2026 voor voice agents?
De revisie van 15 april 2026 maakte het Model Context Protocol eersteklas, voegde een container-sandbox voor tool-code toe en maakte sub-agent-overdrachten een runtime-primitief. Voor voice agents betekent dat een router-agent kan overdragen aan gespecialiseerde sub-agents in plaats van elke tool in één prompt te proppen.