Header Logo

How to Add a Realtime AI Virtual Receptionist to a Plain HTML and Vanilla JS Website

How to Add a Realtime AI Virtual Receptionist to a Plain HTML and Vanilla JS Website

Add a realtime AI receptionist to plain HTML/vanilla JS with iframe embeds, server-side sessions, and LiveKit avatar sync.

Introduction


If you want a realtime AI receptionist on a plain HTML and vanilla JavaScript site, the core problem is not “render an avatar.” It is coordinating three streams at once: microphone audio from the browser, low-latency agent inference, and a synchronized visual face that lip-syncs to the agent’s speech without making the experience feel laggy or brittle.


By the end of this post, you should understand the basic architecture, the browser-side constraints, how to wire a simple webpage to a voice agent, and where Protoface fits when you want to add a talking avatar without rebuilding your frontend stack.


What “realtime receptionist” actually means


For a receptionist flow, the user expects a few things that are easy to underestimate:


  • Low interaction latency: the agent should begin responding quickly enough that turns feel conversational.

  • Audio-first input: the browser captures microphone audio, often via WebRTC or an equivalent streaming transport.

  • Continuous visual feedback: the avatar should be speaking while the agent is speaking, not as a separate prerecorded clip.

  • Stateful turn-taking: the agent needs to know when the user is talking, when to listen, and when to answer.


The important implementation detail is that the video face is not “just UI.” It is a realtime rendering surface driven by the same session that carries the voice interaction. If the speech and face are out of sync, the whole experience reads as fake immediately.


Browser architecture on a plain HTML page


On a vanilla site, there are two practical integration patterns:


  1. Embed an iframe that contains the avatar UI and session logic, keeping your page free of sensitive credentials.

  2. Build your own session UI in JavaScript and connect it to an agent or avatar service through a backend you control.


For a simple receptionist on an otherwise static site, the iframe approach is usually the correct default. It avoids exposing API keys in the browser and removes a lot of transport and lifecycle edge cases. If you already have a backend and want tighter control over the calling flow, you can create sessions server-side and hand the browser only the ephemeral information it needs.


Either way, the browser still has to handle the same fundamentals:


  • Ask for microphone permission.

  • Establish a realtime session.

  • Keep the connection alive while the conversation is active.

  • Cleanly stop and release media tracks on teardown.


Why vanilla JS is enough for the frontend


You do not need a SPA framework to host a receptionist widget. The hard parts are mostly transport and session management, not component composition. A plain page can:


  • Render a container for the avatar.

  • Load an iframe or attach a WebRTC client.

  • Handle a “Start” button that triggers microphone capture.

  • Display fallback UI when permission is denied or the connection drops.


The biggest design choice is where the trust boundary lives. If your browser code has to talk directly to an authenticated API, you are responsible for preventing key leakage. If you use a managed iframe embed, the browser only gets an embed URL or session-specific configuration, and the sensitive parts stay off the client.


A simple iframe-based receptionist


If your goal is “get it working on a plain HTML page,” an iframe embed is the most direct path. The browser page can remain static; the avatar and session handling live inside the embedded surface. That keeps your site’s code simple and avoids having to ship your own media/signaling layer.


A minimal page might look like this:


<!doctype html>

</html>
<!doctype html>

</html>
<!doctype html>

</html>


That example is intentionally generic because the exact embed URL and configuration fields depend on your session settings. The point is architectural: the page itself does not need any API keys, and the browser never sees long-lived credentials.


There are a few operational constraints worth calling out:


  • Allowlist the parent origin so only your site can host the embed.

  • Set voice and instructions per embed if you want receptionist behavior to differ by page or tenant.

  • Use rate limits to keep the experience predictable and avoid accidental abuse.


For a static marketing site or a small business homepage, this is usually enough. You can ship a usable voice receptionist without standing up a custom media backend.


If you need direct API control, create sessions server-side


When you want tighter control over session lifecycle, create avatars or sessions from your backend and hand the browser only the minimum necessary data. The REST API is authenticated with an API key, so it belongs server-side, not in your HTML or JavaScript bundle.


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 \
}'


In practice, you would call this from your own backend, then return a session token, embed URL, or other client-specific connection material according to the documented API. The exact response shape depends on the endpoint, so use the docs for the current contract.


This server-side pattern is what you want when:


  • you need custom authorization logic before creating a session,

  • you want to attach tenant-specific instructions,

  • you are tracking usage per customer or per page, or

  • you need to integrate the receptionist with your existing app state.


Where the LiveKit agent plugin fits


If your receptionist is really a voice agent first and the face is an added requirement, the cleanest path is often to keep your existing LiveKit voice stack and add the avatar in the agent process. Protoface exposes a LiveKit Agents plugin, which means the avatar becomes part of the same realtime session as the voice agent instead of a separate video component glued on afterward.


That matters because lip sync and speaking state should follow the agent’s speech stream, not an independent timer. The plugin handles that synchronization for you. A representative pattern looks like this:


from livekit.agents import Agent

)
from livekit.agents import Agent

)
from livekit.agents import Agent

)


If you are already using LiveKit for streaming audio, this is a better fit than building a separate video subsystem. Keep the voice agent logic where it already lives, and let the plugin provide the speaking face.


For the plugin itself, the relevant repository and examples are on GitHub, and the package is published on PyPI as livekit-plugins-protoface.


Practical gotchas for a production receptionist


Most issues in these integrations are not exotic. They are the usual realtime mistakes:


  • Autoplay policies: browsers often block audio until a user gesture occurs. Make the user click something before starting media.

  • Mic permission failures: always handle denial explicitly and show a retry path.

  • Latency spikes: keep your backend close to the media region and avoid unnecessary round trips.

  • Session cleanup: stop tracks, close connections, and end sessions when the conversation is over.

  • Instruction drift: receptionist prompts should be narrow; long, generic prompts increase variance and make behavior harder to predict.


Also remember that the avatar should be treated as part of the user experience, not as decoration. If the audio is delayed but the face keeps moving, users notice. If the face freezes while the agent is speaking, users notice even more. The visual layer should fail gracefully, not independently.


How Protoface solves the integration problem


For a plain HTML and vanilla JS website, the cleanest Protoface route is the customer-managed iframe embed. You keep your page static, do not expose API keys in the browser, and still get an interactive avatar with session controls, parent-origin allowlisting, and per-embed configuration. That is the right trade-off when you want a receptionist on a marketing site, a support page, or a small web app without introducing a custom realtime backend.


If you need to create or manage sessions programmatically, the REST API and Python SDK are available as the next layer up. If you already have a LiveKit voice agent, the plugin route is better because it preserves your existing agent architecture and adds the synchronized face directly into the agent runtime. Pick the integration surface that matches your current stack instead of forcing everything through the browser.


For implementation details, the current contracts and examples are in the documentation at docs.protoface.com.


Conclusion


Adding a realtime AI virtual receptionist to a plain HTML site is mostly about choosing the right boundary: keep browser code simple, keep secrets off the client, and make sure speech and avatar rendering stay synchronized. For most developers, an iframe embed gets you to production fastest. If you need deeper control, create sessions server-side or integrate the avatar into an existing LiveKit agent.


Start with a narrow receptionist prompt, test in a real browser with microphone permissions, and validate the turn-taking behavior before you tune the visuals. From there, you can iterate on voice, instructions, and session policy without rewriting the frontend.


See the docs at docs.protoface.com and the quickstarts linked from the Protoface repository if you want to adapt the pattern to your own stack.

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.