> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voiceaiwrapper.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting & FAQ

> Requirements, gotchas, and answers to common Headless Mode questions

## Requirements & gotchas

<Warning>
  **Never open your page via `file://`.** The page must be served over `https://` (or `http://localhost` for development). Browsers don't persist microphone permission on `file://` pages, causing repeated permission prompts and calls that die with no audio. This is the single most common integration problem.
</Warning>

### Microphone permission

Mic access is checked via `getUserMedia` *before* any provider connection, on all providers, so you always get a consistent `voice.mic-permission-denied` event. The probe stream is released immediately.

<Tip>
  When permission is denied, guide visitors to the lock icon in the browser's address bar to re-allow the microphone.
</Tip>

### 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.

<Note>
  Safari: microphone permission prompts may be per-session unless the visitor sets "Allow" for the site.
</Note>

### 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 `keepalive` requests 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-denied`
* `voice.error`
* `chat.error`
* `widget.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 *without* `hide-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

<AccordionGroup>
  <Accordion title="Which plans include Headless Mode?">
    Headless Mode is available on our Pro tiers only. You can upgrade your plan from the [dashboard](https://dashboard.voiceaiwrapper.app).
  </Accordion>

  <Accordion title="Do I need an SDK or npm package?">
    No. One script tag plus one custom element. The API is plain DOM methods and events.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Can a visitor be on a call and chatting at once?">
    Yes - the voice and chat surfaces are independent.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="What happens if my button calls startCall() before the widget loads?">
    It's buffered and fires automatically once the widget is ready. Chat messages are queued in order the same way.
  </Accordion>

  <Accordion title="How do I show &#x22;agent is typing&#x22;?">
    Listen to `chat.agent-typing-started` / `chat.agent-typing-stopped`. The pair is always balanced, so your typing indicator can never get stuck.
  </Accordion>

  <Accordion title="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`.
  </Accordion>

  <Accordion title="Is the widget ID a secret?">
    No - it's a public identifier, like a publishable key.
  </Accordion>

  <Accordion title="Does hiding the widget mute the call?">
    No - audio is independent of UI. The call sounds exactly the same with or without `hide-launcher`.
  </Accordion>
</AccordionGroup>

## Still stuck?

[Contact support](/general/support) and include which event (or missing event) you're seeing - the `voice.*` / `chat.*` event stream usually pinpoints the issue quickly.
