Header Logo

Embedding a Realtime Voice Receptionist on a Static Website with WebRTC and JavaScript

Embedding a Realtime Voice Receptionist on a Static Website with WebRTC and JavaScript

Build a static-site voice receptionist with WebRTC, JavaScript, and short-lived sessions, plus synchronized avatar video.

Introduction


If you want a realtime voice receptionist on a static website, the hard part is not the UI. It’s the transport and session model: you need low-latency audio both ways, a way to synchronize speech with a visible face, and a deployment pattern that doesn’t force you to run your own backend just to answer a phone-like interaction in the browser.


This post walks through the practical WebRTC architecture behind that pattern and how to wire it up with JavaScript. By the end, you should understand:


  • what the browser actually needs to participate in a realtime voice session,

  • why WebRTC is the right primitive for this job,

  • how to keep secrets and session control out of static frontend code, and

  • where a service like Protoface fits when you want a voice agent with a synchronized video face.


What “realtime voice receptionist” means in the browser


A voice receptionist is just a bidirectional audio session plus application logic. The agent listens to the user, streams audio to an ASR/LLM/TTS pipeline, and plays the synthesized response back with minimal buffering. If you add a talking face, the avatar video has to stay aligned with the audio stream closely enough that lip motion looks native rather than bolted on.


For a static website, the browser is typically only responsible for:


  • capturing microphone audio with the Web Media APIs,

  • joining a realtime room or session over WebRTC,

  • rendering remote audio/video tracks, and

  • sending lightweight metadata or UI events, such as “start”, “stop”, or “reset conversation”.


The browser should not hold API keys for your avatar platform. That is the mistake that turns a static embed into an account-compromise waiting to happen.


Why WebRTC is the right transport


For conversational latency, you want a transport that does not behave like classical request/response HTTP. WebRTC gives you:


  • Low-latency media over UDP with congestion control tuned for realtime audio/video.

  • Browser-native capture and playback through standard JavaScript APIs.

  • Bidirectional tracks, which is important when the agent is both listening and speaking.

  • Session semantics that map naturally to “one visitor, one receptionist interaction”.


Under the hood, the browser still has to negotiate a peer connection: exchange ICE candidates, establish DTLS-SRTP, and then publish/subscribe to audio or video tracks. In practice, most developers should not implement signaling from scratch unless they need a custom media topology. A room abstraction from an agent framework or an SDK is usually enough.


Basic browser flow with JavaScript


The browser side of a realtime receptionist usually follows this sequence:


  1. Fetch a short-lived session token or join payload from your server.

  2. Initialize the WebRTC client.

  3. Join the session/room.

  4. Publish the local microphone track if the agent expects upstream audio.

  5. Subscribe to the remote agent audio and, if present, the avatar video track.


The exact code depends on the room SDK you are using, but the control flow looks like this:


// Pseudocode: the exact SDK calls depend on your WebRTC provider

});
// Pseudocode: the exact SDK calls depend on your WebRTC provider

});
// Pseudocode: the exact SDK calls depend on your WebRTC provider

});


That pattern is simple, but there are a few operational details worth calling out.


Latency, echo, and the “talking over itself” problem


In a voice receptionist, the failure mode users notice first is overlap: the agent keeps speaking while the visitor is talking, or the mic picks up the avatar’s own speech and feeds it back into the conversation.


Three things matter here:


  • Echo cancellation in the browser: request audio processing where appropriate, especially if the user is on speakers rather than headphones.

  • Turn-taking / interruption logic in the agent: the server side should decide when to pause TTS, barge-in, or continue.

  • Track buffering: avoid extra client-side queues that add subjective delay. Realtime voice should feel like a conversation, not a voicemail box.


For avatar video, lip sync quality depends on the media pipeline keeping the video generation aligned with the spoken audio. If the video is generated independently from the speech stream, even small timing drift becomes obvious. In other words: the avatar layer needs to be coupled to the voice session, not layered on top as a separate animation system.


Security and deployment on a static site


If your site is static, you still need a trusted control plane somewhere. The browser can join a session, but it should not be allowed to mint unlimited sessions, create avatars, or read secret keys. The standard pattern is:


  • the frontend requests a short-lived token or session descriptor from your own backend,

  • the backend signs or brokers that request using your provider credentials, and

  • the browser uses only the ephemeral artifact needed to connect.


That keeps the public surface small. It also lets you enforce per-visitor limits, origin checks, and basic abuse controls. For a receptionist on a marketing site, you usually want sane defaults: per-IP rate limits, time-bounded sessions, and a hard ceiling on concurrent visitors. If you let any page on the internet create arbitrarily many sessions, you have built a billable denial-of-service endpoint.


One practical integration path with Protoface


If your goal is specifically “voice agent plus synchronized face”, the fastest clean path is to keep the browser lightweight and use a managed avatar/session service behind it. Protoface exposes a REST API for creating avatars and realtime sessions, and a Python SDK if you want to orchestrate that from server code. For a static website, the important point is that the browser never sees an API key; your backend mints only the narrow session data the page needs.


A minimal server-side session creation flow using the REST API looks like this:


curl -X POST https://api.protoface.com/v1/sessions \
}'
curl -X POST https://api.protoface.com/v1/sessions \
}'
curl -X POST https://api.protoface.com/v1/sessions \
}'


The response shape and exact fields are documented in the API docs, but the important design pattern is stable: create the session on the server, pass only the ephemeral connection details to the browser, then connect the page to that session with WebRTC.


If you prefer Python, the SDK fits naturally into a small backend endpoint:


from protoface import Client

return {"session_url": session.url, "token": session.token}
from protoface import Client

return {"session_url": session.url, "token": session.token}
from protoface import Client

return {"session_url": session.url, "token": session.token}


For teams already using LiveKit-based voice agents, the plugin route is often the cleanest way to add the face without changing the voice stack. The quickstart examples are useful if you want to see the whole flow end to end, and the docs at docs.protoface.com cover the session and embed model in more detail.


What to watch for in production


There are a few failure modes that are easy to miss during development:


  • Mobile autoplay restrictions: remote audio may need a user gesture before playback starts.

  • Disconnect handling: the UI should recover gracefully if ICE fails or the network changes mid-session.

  • Avatar size and visibility: if the face is too small, users will not perceive lip sync quality; if it is too large, layout and performance suffer.

  • Session cleanup: close stale sessions aggressively to avoid leaking usage and confusing analytics.


Also test with real microphones, not just laptop speakers. The audio path that feels “fine” on a desk can behave very differently once a user is in a noisy room or on a phone connection.


Conclusion


A realtime voice receptionist on a static site is mostly a session-management problem, not a frontend trick. WebRTC gives you the media transport, JavaScript gives you browser capture and rendering, and the backend keeps credentials and policy where they belong. If you also want a visible, synchronized avatar, make sure the video is tied to the same realtime conversation rather than animated independently.


If you are building this today, start with the docs, wire up a short-lived session broker on your server, and keep the browser side thin. From there, you can iterate on turn-taking, branding, and prompt behavior without changing the media architecture.


For implementation details, examples, and the exact session fields, see docs.protoface.com.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.