Are you an LLM? You can read better optimized documentation at /docs/embedding/events.md for this page in Markdown format
Events
When a customer sends a message, your own analytics code should know about it. When the assistant replies, you might want to update a notification badge, log the exchange, or quietly hand off to another part of your UI. The widget SDK exposes a small, stable set of events so your code can react without polling or guessing.
You'll subscribe with Perfox('on', event, callback) and unsubscribe with Perfox('off', event, callback) — using the same callback reference each time. This page covers every event, how the async reply model works, and a short end-to-end example.
Prerequisites: you already have the widget embedded and Perfox available on the page. If not, start with Quickstart first.
Event reference
The table below is the complete public event surface.
| Event | Payload | Fires when |
|---|---|---|
ready | { workflow_id?, ui?, capabilities?, tab_id? } | The widget's first /init resolves — the widget is mounted and ready. |
user | user context object or undefined | After you call identify, update, shutdown, or boot with a user. |
shutdown | none | Perfox('shutdown') is called. |
show | none | Perfox('show') makes a hidden bubble visible. |
hide | none | Perfox('hide') hides a visible bubble. |
open | none | The panel expands — the visitor clicks the bubble, or you call Perfox('open'). |
close | none | The panel collapses — the visitor clicks close, or you call Perfox('close'). |
message:sent | { text } | The customer presses send — fires before the request hits the backend. |
message:received | { text } | The assistant reply (or an error message shown in the chat) lands in the browser. Replies with no text, such as a card-only reply, don't fire it. |
ui:command | { event, payload, meta } | The agent or your server sends a UI command to this page. Most pages use Perfox('on:ui', name, handler) instead of listening to this directly. |
The capabilities field on ready carries the switches set on your Web Chat trigger: voice_chat, dial_out, file_upload, plus show_history, allow_new_chat and allow_export — see Voice & Dial-Out and File Upload. ui carries the appearance you configured (display name, avatar, colour, bubble position, greeting, starters). tab_id identifies this browser tab, so your server can target a UI command at one tab.
The widget's internal "thinking" state (the typing indicator) is not exposed as an event. If you need a loading state in your own UI, derive it from message:sent → message:received — set a flag on sent, clear it on received.
Worked example: Acme Support's engagement tracking
Setup. Asha, an admin at Acme Support, has embedded the widget on the help centre. She wants to track chat opens and replies in her analytics tool, and she wants the page to show a small badge while a reply is in flight.
Action. She adds this after the embed script:
js
// Know when the widget is ready to accept interactions
Perfox('on', 'ready', (info) => {
console.log('Perfox ready', {
workflow_id: info.workflow_id,
capabilities: info.capabilities, // { voice_chat, dial_out, file_upload, … }
});
});
// Track engagement
Perfox('on', 'open', () => analytics.track('chat_open'));
Perfox('on', 'close', () => analytics.track('chat_close'));
// Show a badge while waiting for a reply, clear it when the reply lands
Perfox('on', 'message:sent', () => { document.querySelector('#chat-badge').hidden = false; });
Perfox('on', 'message:received', (m) => {
document.querySelector('#chat-badge').hidden = true;
analytics.track('chat_reply', { text: m.text });
});
// React to the customer logging out
Perfox('on', 'shutdown', () => console.log('widget reset'));Result. When Priya, a customer, opens the chat panel and sends a question, the badge appears immediately. A moment later the assistant replies, the badge disappears, and the exchange is recorded in Asha's analytics dashboard.
What just happened. message:sent fires the instant Priya presses send — before the network round-trip. message:received fires when the reply actually arrives in the browser. That gap is where your "waiting" UI lives.
Unsubscribing
Hold on to the callback reference you passed to on and give it to off when you no longer need the listener:
js
const onReply = (m) => analytics.track('chat_reply', { text: m.text });
Perfox('on', 'message:received', onReply);
// Later — for example, when the user navigates away from the page
Perfox('off', 'message:received', onReply);Passing an anonymous function to off does not work — it will not match the original listener. Always save the function to a variable first.
How message:received works with async replies
The widget keeps a WebSocket open to Perfox. When the visitor sends a message, the request returns 202 Accepted almost immediately — the turn is queued but the agent hasn't answered yet. When the agent finishes, the reply is pushed to the browser over the socket and the widget fires message:received at that point. While the agent is still writing, the text may already stream into the chat bubble; message:received fires once, with the final text.
This means message:received can fire noticeably later than the send — and that is expected. The widget manages the connection for you, including recovering a reply whose push was missed.
If the socket isn't available, the widget sends synchronously and receives the reply in the HTTP response body. From your event listener's point of view both modes look identical: you always hear message:sent then message:received.
You can now listen to every user interaction in your widget
You've seen the full event surface, how to subscribe and unsubscribe cleanly, and how to handle the async gap between send and receive.
Next steps:
- window.Perfox SDK — the complete API reference for every command you can call on the widget, including
identify,show,hide, andshutdown. - UI-command bus — let the agent drive your page, and handle those commands with
Perfox('on:ui', …). - Quickstart — if you haven't embedded the widget yet, start here to get the script on your page.