Requirements & gotchas
Microphone permission
Mic access is checked viagetUserMedia before any provider connection, on all providers, so you always get a consistent voice.mic-permission-denied event. The probe stream is released immediately.
Audio output
hide-launcher hides UI only - audio is unaffected. The provider SDKs attach their audio elements to document.body, so hiding the widget never mutes the call.
Browser support
Anything with Custom Elements and WebRTC - all evergreen browsers.Safari: microphone permission prompts may be per-session unless the visitor sets “Allow” for the site.
Single-page apps
The element works inside React, Vue, and Next.js layouts. Keep in mind:- Removing the element from the DOM destroys the instance and ends active sessions. For client-side routed apps, mount it once at the layout level if conversations must survive navigation.
- Page navigation or refresh ends calls and chat sessions. Chat cleanup uses
keepaliverequests so server logs close properly.
Error handling is your job
In Headless Mode there is no widget UI to show errors. Handle at minimum:voice.mic-permission-deniedvoice.errorchat.errorwidget.error
The widget ID is public
It’s visible in any embedding page’s source by design; the config endpoint is public. No secrets are exposed - provider private keys never reach the browser, and conversation tokens are minted per-conversation by the backend.Visible widget + API together
The JS API also works withouthide-launcher - methods and events function alongside the normal launcher. Headless Mode is just the “renders nothing” variant.
Per-embed, not per-campaign
hide-launcher is an attribute on the embed snippet. The same widget can be embedded normally on page A and headless on page B.
FAQ
Which plans include Headless Mode?
Which plans include Headless Mode?
Headless Mode is available on our Pro tiers only. You can upgrade your plan from the dashboard.
Do I need an SDK or npm package?
Do I need an SDK or npm package?
No. One script tag plus one custom element. The API is plain DOM methods and events.
Does it work with React / Vue / Next.js / Webflow?
Does it work with React / Vue / Next.js / Webflow?
Yes - grab the element (ref or
querySelector), call methods, add event listeners. No framework bindings needed.Which providers does it support?
Which providers does it support?
All of them (Vapi, Retell, ElevenLabs) with an identical API. You don’t need to know or care which provider the campaign uses.
Can a visitor be on a call and chatting at once?
Can a visitor be on a call and chatting at once?
Yes - the voice and chat surfaces are independent.
Can I still use the normal floating widget on other pages?
Can I still use the normal floating widget on other pages?
Yes -
hide-launcher is per-embed. Embed the same widget normally on one page and headless on another.What happens if my button calls startCall() before the widget loads?
What happens if my button calls startCall() before the widget loads?
How do I show "agent is typing"?
How do I show "agent is typing"?
Listen to
chat.agent-typing-started / chat.agent-typing-stopped. The pair is always balanced, so your typing indicator can never get stuck.Why did my chat session end by itself?
Why did my chat session end by itself?
Either the inactivity timeout (configurable per widget, default 3 minutes) or a tab switch - check the
reason field on chat.session-ended.Is the widget ID a secret?
Is the widget ID a secret?
No - it’s a public identifier, like a publishable key.
Does hiding the widget mute the call?
Does hiding the widget mute the call?
No - audio is independent of UI. The call sounds exactly the same with or without
hide-launcher.Still stuck?
Contact support and include which event (or missing event) you’re seeing - thevoice.* / chat.* event stream usually pinpoints the issue quickly.
