Skip to main content
All events are standard DOM CustomEvents dispatched on the <voiceai-widget> element (or on the init() container). Subscribe with addEventListener - it works in vanilla JS, React (via a ref), Vue, Svelte, Webflow custom code, anything. The payload is in event.detail.
Every voice.* / chat.* payload (and widget.ready) includes provider: 'vapi' | 'retell' | 'elevenlabs'. It is a debug aid only - keys and semantics are identical regardless of its value. Never branch on it.

widget.* - lifecycle / shared

voice.* - calls

chat.* - messaging

chat.session-ended reasons

When a session ends with reason: 'inactivity', show something like “Session expired - send a message to start a new one.” The next sendChatMessage() starts a fresh session automatically.
Chat sessions are ephemeral: there is no history persistence across page loads, by design.

Guarantees & event ordering

These guarantees are enforced by an internal sequence guard - they do not depend on provider SDK behavior.

Voice state machine (per call attempt)

  • voice.ended fires exactly once per voice.connecting - never zero times, never twice.
  • Duplicate or out-of-order provider SDK events are dropped.
  • voice.error is informational and never replaces the terminal event.

Chat turn sequence

  • The typing pair is always balanced; -stopped is guaranteed before chat.session-ended.
  • chat.session-ended fires exactly once per chat.session-started.
  • For streaming providers, assistant text streams internally, but the public stream still emits one chat.message per completed reply - identical to non-streaming providers.

Cross-provider conformance

The same integration code produces the same event names, payload keys, and ordering whether the campaign runs on Vapi, Retell, or ElevenLabs. Provider quirks - different SDK event names, batched replies, echoed user messages, missing typing signals, varying error shapes - are all normalized away before events reach your page.