Build a Voice Assistant with the OpenAI Realtime API and WebRTC A tutorial by Rachel Goldstein shows how to build a browser-based voice assistant using the OpenAI Realtime API and WebRTC, with a Node.js server that mints ephemeral tokens and a static page that streams audio both ways. The assistant uses the gpt-realtime-2.1 model, supports interruptible voice chat, and includes a function call to fetch the user's local time. Build a Voice Assistant with the OpenAI Realtime API and WebRTC Wire a browser to gpt-realtime-2.1 over WebRTC for interruptible voice chat with function calling. Rachel Goldstein https://sourcefeed.dev/u/rachel goldstein What you'll build A browser-based voice assistant you can talk to — and interrupt mid-sentence — built on the OpenAI Realtime API https://developers.openai.com/api/docs/guides/realtime over WebRTC https://webrtc.org , with a function call the model invokes to fetch your actual local time. You'll end with a zero-dependency Node.js https://nodejs.org server that mints ephemeral tokens and a static page that streams audio both ways. Prerequisites - Node.js 20 or later you need the built-in global fetch ; tested with Node 22 LTS - An OpenAI account with billing enabled and an API key exported as OPENAI API KEY — Realtime sessions bill per audio token, so expect a few cents for this tutorial - A current desktop browser Chrome, Edge, Firefox, or Safari and a working microphone - Verified July 2026 against the GA Realtime API and the gpt-realtime-2.1 model No npm packages required — the server uses only Node built-ins. 1. Scaffold the project mkdir voice-assistant && cd voice-assistant mkdir public printf '{ "name": "voice-assistant", "type": "module" }\n' package.json export OPENAI API KEY="sk-..." your real key The package.json exists only to enable ES modules; there's nothing to install. 2. Mint ephemeral tokens on the server Your API key must never reach the browser. Instead, the server calls POST /v1/realtime/client secrets to create a short-lived client secret an ek ... string, 10-minute default TTL that the browser uses to connect directly to OpenAI: sequenceDiagram participant B as Browser participant S as Your Node server participant O as OpenAI Realtime API B- S: GET /token S- O: POST /v1/realtime/client secrets API key O-- S: ephemeral key ek ... S-- B: ephemeral key B- O: POST /v1/realtime/calls SDP offer + ek ... O-- B: SDP answer B<<- O: WebRTC: mic audio ⇄ model audio + "oai-events" data channel Create server.js . It serves the static files and exposes /token . The session config — model, voice, instructions, and the tool definition — lives here, baked into the client secret at creation time, so the browser can't tamper with it: js import { createServer } from "node:http"; import { readFile } from "node:fs/promises"; const SESSION CONFIG = { session: { type: "realtime", model: "gpt-realtime-2.1", instructions: "You are a concise, friendly voice assistant. " + "When the user asks about the time, call get current time and " + "answer using its result.", audio: { output: { voice: "marin" } }, tools: { type: "function", name: "get current time", description: "Get the user's current local time and timezone.", parameters: { type: "object", properties: {}, required: }, }, , tool choice: "auto", }, }; const server = createServer async req, res = { if req.url === "/token" { 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 CONFIG , } ; res.writeHead r.status, { "Content-Type": "application/json" } ; res.end JSON.stringify await r.json ; return; } const path = req.url === "/" ? "/index.html" : req.url; try { const file = await readFile ./public${path} ; const type = path.endsWith ".js" ? "text/javascript" : "text/html"; res.writeHead 200, { "Content-Type": type } ; res.end file ; } catch { res.writeHead 404 ; res.end "Not found" ; } } ; server.listen 3000, = console.log "Voice assistant running at http://localhost:3000" ; 3. Build the page Create public/index.html . The Connect button matters beyond UX: starting from a click satisfies browser autoplay policy, so the model's audio is allowed to play. < DOCTYPE html