Widget events Live
The widget emits lifecycle events you can subscribe to from your page. Use them to wire analytics, trigger UI changes, or chain extra automation on top of the chat.
Subscribing
Use Message.on(name, handler) to subscribe and Message.once(name, handler) for a one-shot listener. The return value of both is an unsubscribe function. Handlers receive a single payload object whose shape depends on the event.
// Subscribe to an event. Returns an unsubscribe function.
const off = Message.on('message:received', (msg) => {
console.log('agent reply:', msg.body);
});
// Later, stop listening.
off();
// One-shot listener: fires once, then unsubscribes itself.
Message.once('ready', () => {
console.log('widget booted');
});Subscriptions are local to the browser session and not persisted. Re-register them on every page load (or inside your framework's mount lifecycle).
Lifecycle events
| Event | Fires when | Payload |
|---|---|---|
ready | Bundle has loaded and public config has resolved. | None. |
open | Panel is opened (programmatically or by click). | None. |
close | Panel is closed. | None. |
conversation:started | The visitor sends their first message in a session. | Conversation object. |
conversation:assigned | An agent picks up the conversation. | Conversation with assignedAgent. |
conversation:closed | Agent or visitor closes the conversation. | Conversation object. |
message:sent | Visitor sends a message. | Message object. |
message:received | Agent reply arrives. | Message object with agent details. |
agent:typing | Agent typing indicator starts. | { conversationId, agentId } |
agent:typing-stopped | Agent typing indicator clears. | { conversationId, agentId } |
visitor:identified | Successful identify call (server confirmed). | Visitor record. |
error | Transport, auth, or rate-limit error. | { code, message } |
Payload shapes
Payload examples for the most common events. The full set of fields is documented in the Messages API.
// message:received
{
id: 'uuid',
conversationId: 'uuid',
body: 'Plain text reply',
bodyHtml: '<p>Sanitized HTML</p>',
agent: { id: 'uuid', name: 'Sam', avatarUrl: '...' },
createdAt: '2026-05-13T15:30:00Z'
}
// conversation:started
{
id: 'uuid',
channel: 'chat',
status: 'open',
createdAt: '...'
}
// conversation:assigned
{
id: 'uuid',
assignedAgent: { id: 'uuid', name: 'Sam', avatarUrl: '...' }
}
// agent:typing
{
conversationId: 'uuid',
agentId: 'uuid'
}
// error
{
code: 'connection_failed' | 'auth_failed' | 'rate_limited',
message: 'Human-readable description'
}Wiring analytics
Conversation started, first response received, and conversation closed are the highest-signal events for buyer-intent and CSAT instrumentation. Fire them into your analytics tool of choice.
Message.on('conversation:started', (conv) => {
// Fire analytics. Buyer-intent signal.
gtag('event', 'chat_started', {
conversation_id: conv.id,
page: location.pathname
});
});
Message.on('message:received', (msg) => {
// Track agent response latency.
if (window.__msgStarted) {
const ms = Date.now() - window.__msgStarted;
gtag('event', 'first_response', { duration_ms: ms });
}
});For more on chat-to-paid attribution, see Track chat-to-paid conversion.
Server-side equivalents
If you need the same events server-side (for example, to log a conversation into your data warehouse), use outgoing webhooks. The widget events listed here mirror the relevant subset of agent-namespace Socket.io events. See Agent namespace and Webhooks overview.
Common pitfalls
- Listening before ready. Calling
Message.onbefore the bundle loads throws. UseMessageOnReadyto defer registration. - Leaking handlers. In a single-page app, register listeners inside
onMountand remove them inonUnmount. Without cleanup, listeners stack up across route changes. - Treating events as the source of truth. Events fire optimistically. For audit-grade data, use the REST API timeline at Conversations.
- Unknown event names. Subscribing to an event we do not emit is a silent no-op. Typos do not throw.