- A Node relay that serves the page and holds your API key, so the browser never sees it.
- A page that streams your microphone in, plays the concierge’s spoken reply, and stops when you interrupt.
- The same
check_availabilityspa tool, answered by the page mid-conversation. - A quick check that the relay serves only the page and refuses other websites.
- Your coding agent, set up as in the quickstart’s Set up your agent: the skill installed,
POLY_API_KEYset, and the connection check passing. - The
voice_agent.pyyou built in the quickstart for coding agents, with itscheck_availabilitytool. Run these prompts in the same folder. - Node 22 or later.
- A desktop browser and a microphone. Use headphones.
Step 1: build the relay and the page
Prompt
browser/:
- The relay, a Node server on the
wspackage. It serves the page and accepts the page’s WebSocket on a path such as/realtime-relay. For each page that connects, it opens its own connection towss://api.studio.poly.ai/v1/realtimewith your key in theX-API-KEYheader, then passes text frames both ways. It holds anything the page sends before that connection is open. A browser can’t set headers on a WebSocket, so the page can’t authenticate on its own. The relay is the only part that ever has the key. - The page, plain JavaScript with no build step. It speaks the same protocol as
voice_agent.py. The click that starts the call also starts theAudioContext, which browsers only allow during a click. The microphone comes fromgetUserMediawith echo cancellation, and anAudioWorkletturns its samples into PCM16, sent asinput_audio_buffer.appendframes of about 20 ms. Browsers capture at their own rate, usually 48 kHz, sosession.updatesetsaudio.input.format.rateto theAudioContext’ssampleRate. Reply audio arrives asresponse.output_audio.delta, 24 kHz PCM16, and plays as buffers scheduled back to back. Wheninput_audio_buffer.speech_startedarrives, the page stops every scheduled buffer at once.
check_availability definition in session.tools. When response.function_call_arguments.done arrives, it runs the function, sends the result as a function_call_output item, then sends response.create, all through the relay. That keeps the relay a plain pipe, as in the guide, and made-up slots need no secrets. A real booking system with its own credentials belongs on your server: have the tool call your own API, or answer tool calls in the relay.
Agents usually lock the relay down too, without being asked. It listens only on your own machine, and refuses a WebSocket whose Origin isn’t its own page. Step 2 checks that.
Check it. Start the relay:
http://localhost:8787. Click the button to start the call and allow the microphone. Ask “Is the spa free on Friday?” You should hear the free slots read back, and see the tool call and both sides of the conversation on the page. Your own words appear once the reply has started: the transcript of what you said arrives during the reply. Then talk over the concierge while it’s speaking. It should stop at once and answer your new question.
To see what the browser sends, open the developer tools and filter the Network tab to WebSockets. There’s one socket, to your relay on localhost, not to api.studio.poly.ai. Once the call is set up, its messages show input_audio_buffer.append frames streaming the whole time, even while you’re quiet.
If it goes wrong:
Step 2: check the key stays on the server
Prompt
npm run security. The relay serves files only from the page’s folder, so requests for server.js, .env or ../ paths get a 404. It accepts a WebSocket only when the Origin is its own page, and refuses others with a 401 or 403. Expect a fix or two: agents often find that a domain pointed at your machine could reach the relay, and limit it to localhost.
The
Origin check stops other websites, not other programs: anything that isn’t a browser can send any Origin it likes. That’s fine on your own machine. Before you put the relay online, put it behind your app’s sign-in. Anyone who can reach it is spending your balance.RELAY to the http://localhost address your relay printed:
404, then 401 or 403. If your relay uses another WebSocket path, change /realtime-relay to match. Then talk to the page again to make sure the fixes didn’t break it.
Troubleshooting
For401, 402 and 429 on connect, see the quickstart’s troubleshooting. The relay logs them, and agents usually show them on the page too.
Every error code is in the reference.

