Build an AI Voice Assistant with Twilio Voice and Media Streams, OpenAI's GPT-Live API, and Node.js
Time to read:
We're so excited that our friends at OpenAI have launched GPT-Live-1 in the OpenAI API! GPT-Live-1 is the first model in their new GPT-Live family – the speech-to-speech models behind the new ChatGPT Voice.
GPT-Live changes how you can build agents with speech-to-speech models. It runs a conversation across two models:
- a speech-to-speech model owns the audio - listening, talking, and handling interruptions
- a reasoning model does the thinking - calling tools where needed
OpenAI calls the handoff process delegation. Practically, your caller talks with a responsive voice model while another model stands by in the background for when you need deeper reasoning, web search, or more complex work done.
In this tutorial, I'll show you how to build an AI voice assistant powered by OpenAI’s GPT-Live-1 in their API. You can call it from your phone, carried by Twilio Programmable Voice and Twilio Media Streams, with a server written in Node.js. The assistant will answer phone calls, greet callers, and carry on a thrilling, avian-and-rock-legend-tinged conversation. The agent can also search the web, and make tool calls to functions you define.
Enough preening – let's fly! 🦉
Prerequisites and common pitfalls
To follow along with this tutorial, you'll need:
- A Twilio account – sign up for a free account if you don't yet have one.
- A Twilio phone number with Voice capability – here's how to search and buy a phone number.
- An OpenAI account with GPT-Live-1 access:
- An API key with permission to use the
gpt-live-1andgpt-5.6-terramodels.
- An API key with permission to use the
- Node.js 22 or later – I used
24.5.0writing this tutorial. - A way to expose localhost to the internet – ngrok is the usual choice.
- A phone. Seriously, any phone… you're going to call your agent pretty soon!
Let's do this.
Build the app
Step 1: Initialize the GPT-Live agent project
Fire up your console – it's time to start a project.
Make a directory and initialize it (so npm has a package.json where it can write dependencies):
Step 2: Install dependencies
We need a web server that speaks HTTP and WebSockets. That's a perfect job for Fastify, which handles the server side (along with ws handling the client side). We'll also use dotenv to keep your credentials out of the code.
For reference, here's what I had installed:
Step 3: Create the project files
Now, you need two files – one for secrets, one for the server code. And we're using ES modules in this tutorial, so go ahead and add "type": "module" to your package.json while you're in there anyway:
Put your OpenAI API key in .env:
Add the module type to package.json:
Step 4: Set up your imports and configuration
Like any other Node project, we'll start with some imports.
Open live-demo.js and paste this:
Here's how we're going to use these:
MODELis the speech-to-speech model that talks to your caller.DELEGATED_MODELis the reasoning model it hands hard questions to.VOICEis the voice your caller actually hears. I usedmarin– see OpenAI’s docs for the rest.USER_AGENTis how your app introduces itself to OpenAI, in the form [company or library]/[language] [version]. Put your own name in this one, though I did supply an example…PORTis the port your server listens on, read from .env with a fallback to5050.
My take with this pattern? It’s convenient! You can swap either model or the voice without touching another line, which is handy when you're tuning the agent’s performance later.
Step 5: Write your prompts
Because we'll be dealing with a couple of models, we will set different instructions based upon which job they'll be doing. Paste these next:
VOICE_PROMPT shapes your agent's identity and personality – it controls how your assistant sounds, what it says it can do, and (sometimes) which jokes it'll tell. Notice we lead the prompt with the AI disclosure – if a caller asks later who they're talking to, this is what the model draws on.
BACKEND_PROMPT goes to the delegation model and defines behavior when that model is needed – here, we're instructing it on which tool to reach for and how long an answer to give. And asking for a sentence or two? Since this conversation is by voice, we're keeping it tight!
And, you guessed it, OPENING is your assistant's first spoken line, verbatim.
Step 6: Define the tools your assistant can call
With the instructions out of the way, next up, we're going to set up our tool calls.
{ type: 'web_search' } is the configuration for hosted web search – we're using OpenAI's internal tool here.
get_top_headline, on the other hand? That's yours – treat it as a template or a pattern, though, since I assume you don't want your production agent to make city-simulator game inspired jokes. Whatever tool calls you make should house your own business logic.
With get_top_headline, the description and parameters dictate how the reasoning model decides what to call – and if it picks your tool, what to pass. You should write them as you would documentation for a colleague who can't see your code.
Now, I'll show you the (currently silly) function itself, plus some headlines for it to return. Paste these lines next:
Reticulating splines, indeed!
The function is pretty boilerplate, but for this demo I'll note the two-second delay is deliberate. Your real tools might hit a database or a slow API, so a demo where everything returns instantly isn't a real-life demo. (City simulator 🤝 Agent simulator.)
Step 7: Answer the call with TwiML
Okay, and now we're on to the part where your agent leaves the nest and picks up the phone.
When someone dials your Twilio number, Twilio doesn't know what to do unless you tell it – that's where you answer with TwiML, Twilio's Markup Language. The verb we want is <Connect><Stream> – it opens a bidirectional WebSocket for the life of the call.
The / route is a heartbeat, so you can confirm your tunnel works in your browser before you spend a phone call on it. (Try opening it.)
/incoming-call is the webhook you'll point your Twilio number at in Step 10. Twilio requests it the moment a call arrives, and whatever TwiML you return is what happens on that call – here, <Connect><Stream> hands the audio over to the WebSocket route you'll write next. It's registered with fastify.all so it answers whether Twilio sends a GET or a POST.
The x-forwarded-host check on the first line deserves a moment, too. You have to tell Twilio which wss:// URL to dial, and you can build it from the request's Host header – or, in the case of a forwarded tunnel (like you are probably using for this tutorial) x-forwarded-host. We read x-forwarded-host first, then fall back to Host.
Step 8: Open the GPT-Live session
Now it's time for the more interesting part… you'll register the WebSocket route, then reach out to OpenAI. Paste this next:
The second header there is a request from OpenAI: identify your app with a User-Agent so they can tell your traffic apart from everyone else's. Mine says Twilio because this is a Twilio demo – yours should reflect your app.
Next, configure the session. This is the message that sets up everything we defined in Steps 4 through 6 – that is, the personality, voice, and audio format, plus details for how delegation should work:
Got a second to talk? Let's discuss this code a little more.
startSession guards the handshake against races. It checks sessionRequested, and ensures the OpenAI socket is open and streamSid (Twilio’s call ID) is present. This prevents race conditions, and ensures your session is configured once.
session.start is the handshake. It carries session.model and the session configuration, and GPT-Live-1 replies with session.started when ready. That reply is where you make the assistant talk first – and where you make it say your OPENING verbatim. I'll show that in Step 9.
delegation is OpenAI’s two-model setup. type: 'responses' routes turns to the Responses API when needed. Everything nested under responses (model, instructions, and tools) belongs to the reasoning, or delegation, model.
Step 9: Relay events between Twilio and OpenAI
Now we come to the event logic.
Five branches, one bird 🦉. Let's discuss them:
session.startedis GPT-Live-1 confirming it's ready – and it carries the sessionid, which OpenAI needs if you ever ask them about a call. Right after that ID lands, we send two events:session.instructions.appendwith a "say this verbatim" directive, andsession.commentary.appendto trigger the model into speaking. Together they make GPT-Live-1 open the call with your exactOPENING– so the AI disclosure lands word-for-word (handy for compliance), and the assistant speaks first instead of waiting for the caller.session.output_audio.deltacarries your assistant's voice – the base64 μ-law bytes are inevent.delta, and we hand them to Twilio unchanged.- The
function_callbranch is where delegation calls come back. Function events arrive inside aresponse.eventenvelope – dispatch on the innerevent.typeand read the completed function item'scall_id,name, and JSONarguments. Run your function, then answer with two events:response.item.createto append thefunction_call_output, andresponse.createto continue the response. session.output_transcript.deltais the text of whatever your assistant just said, in 200 ms increments – handy for logs and captions.errorlogs and keeps going – deliberately.
Now, the Twilio side:
Twilio's start event is the other half of the handshake in Step 8. Here we grab the streamSid and try to start the session again.
media frames are the caller's voice (gated on sessionReady so you're not shouting at a session that hasn't opened yet), while stop means the call ended.
And finally, the cleanup handlers (and the server start). Paste this next:
And that's the entire application! On to the run...
Step 10: Expose your server and configure Twilio
Go ahead and start your server:
You should see:
Now expose port 5050. With ngrok, you would run:
Copy the forwarding hostname (without the scheme) – it'll be something like abc123.ngrok.app. Open it in a browser and confirm:
If you see that, your tunnel is public and your server is alive! Of course, if you get a login page or a warning screen instead, fix that before you go any further – Twilio can't click through.
Next, we will point your number at it. In the Twilio Console, go to Phone Numbers → Manage → Active numbers, click your voice-capable number. Then, under Voice Configuration, set A call comes in to Webhook with:
(Swapping in your server from above.) Leave the method as HTTP POST and Save.
Run and test your assistant
We're almost there, can you feel it? Well, even if you can't... you're about to hear it.
Dial your Twilio number. Your agent should pick up and greet you. Wait for the agent to finish, then try a few scenarios:
Ask what your agent can do. It's nice to know if your agent knows its own powers!
Ask for a headline. "What's the top news headline for Boston, Massachusetts?" This is how to test your tool call – though, of course, you can pick a city other than Boston (but why would you want to?).
Ask for a fact. "Who wrote A Tale of Two Cities, and what year was it published?" Again, this should flex a tool call, except this time OpenAI's hosted web_search tool (you won't see delegation instructions in the terminal).
Interrupt it. I like to ask the agent to count to twenty, then cut it off. If you ask where it counted to, it should be reasonably close (within a few numbers).
Isn't that a hoot?
Troubleshooting
The call connects but I hear nothing. Check that your TwiML contains a public hostname: curl -X POST https://your-tunnel/incoming-call and read the wss:// URL.
Missing OpenAI API key. Your .env isn't being read, or you mispasted your key. Confirm the .env file sits in the same directory as live-demo.js and you have a working OpenAI API Key in there (with access to GPT-Live and the delegation model, which is gpt-5.6-terra if you haven't tweaked it).
Something looks wrong on OpenAI's side. Copy the session ID your terminal printed when the call started and include it when you ask OpenAI for help. Sessions that arrive through a provider are hard to locate without it.
Conclusion
Congratulations! You built an AI voice assistant on GPT-Live-1 that answers a phone call, greets you, and carries on a conversation. It also searches the web, calls your own code, and has a predilection for 80s British rockers. And all that in under 200 lines!
And now, the code is ready for your modifications: swap in your business logic, rewrite the prompts, and enjoy GPT-Live. And I bet you will…
Additional resources
- OpenAI's Voice Products
- OpenAI’s GPT-Live-1 Docs
- OpenAI's GPT-Live announcement
- Twilio and GPT-Live in the OpenAI API Resources
- Twilio Programmable Voice documentation
- Twilio Media Streams documentation
- The
<Stream>TwiML noun
Paul Kamp is the Technical Editor-in-Chief at Twilio. He still regrets cutting the Transit Authority budget in SimCity 2000 (yes, he was warned). He can be reached at pkamp [at] twilio.com.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.