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
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.endedfires exactly once pervoice.connecting- never zero times, never twice.- Duplicate or out-of-order provider SDK events are dropped.
voice.erroris informational and never replaces the terminal event.
Chat turn sequence
- The typing pair is always balanced;
-stoppedis guaranteed beforechat.session-ended. chat.session-endedfires exactly once perchat.session-started.- For streaming providers, assistant text streams internally, but the public stream still emits one
chat.messageper completed reply - identical to non-streaming providers.

