Server Events
Server-to-client events published during WebRTC and WebSocket sessions
The server sends events to your client throughout a session. Events notify you of state changes, speech activity, and conversation content as it happens.
Listening for events
Receive events via onData or WebSocket messages
Event types
The four events the server publishes
avatar_state_changed
Fires when the agent's readiness state changes
talk_state_changed
Fires when the agent starts or stops speaking
message_received
Tracks conversation messages through their lifecycle
ui_update
Arbitrary JSON your own functions server pushes to the client
Listening for events
WebRTC (Web SDK)
Pass an onData callback when initializing the SDK. Every server event is delivered through this handler:
const instance = await NapsterCompanionApiSdk.init(token, {
onData: (data) => console.log("Event received:", data),
});WebSocket
Listen for messages on the WebSocket connection. Each message is a JSON-encoded event:
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Event received:", data);
};Event types
| Event | Description |
|---|---|
avatar_state_changed | The agent's readiness state changed |
talk_state_changed | The agent's speaking state changed |
message_received | A conversation message event with an action field describing the lifecycle stage |
ui_update | Arbitrary JSON pushed by your own functions server — the platform relays it to the client untouched |
avatar_state_changed
Fires when the agent's readiness state changes.
{
"event": "avatar_state_changed",
"data": {
"state": "ready",
"timestamp": "2026-07-22T08:03:14.309123+00:00",
"details": { "mode": "audio_only" }
}
}| State | Description |
|---|---|
preparing | The agent is initializing |
ready | The agent is ready to receive input |
data.details, when present, describes what became available — for example mode: "audio_only" or mode: "audio_video".
ready can fire more than once per session. On WebRTC with videoPolicy: "deferred", the session becomes ready with audio first and reports ready again when the video avatar comes up — see Video policy. Latch on the first ready instead of re-running session-start logic on every occurrence.
talk_state_changed
Fires when the agent starts or stops speaking.
{
"event": "talk_state_changed",
"data": {
"state": "started"
}
}| State | Description |
|---|---|
preparing | The agent is preparing to speak |
started | The agent is actively speaking |
ended | The agent has finished speaking |
message_received
The primary event for tracking the conversation. The conversation fields are nested under data.message:
{
"event": "message_received",
"data": {
"message": {
"type": "message",
"role": "user | assistant",
"action": "string",
"response_id": "string",
"item_id": "string",
"content": "string",
"content_index": 0,
"previous_item_id": "string",
"reason": "string",
"error": "string",
"tokens": 0,
"is_forced": false,
"timestamp": "2026-03-04T12:00:00Z"
}
}
}The same structure applies on both WebSocket and WebRTC. Over WebRTC, the Web SDK delivers the event to your onData handler exactly as it arrives on the wire.
Not every field is present in every event. The fields included depend on the action.
| Field | Type | Description |
|---|---|---|
type | "message" or "session" | Frame kind. A "session" frame is a session marker with no role/content — skip it (see below) |
role | "user" or "assistant" | Who the message is from |
action | string | The lifecycle stage of the message |
response_id | string | Groups related events in the same response |
item_id | string | Unique identifier for this conversation item |
content | string | Text content (transcription or response text) |
content_index | number | Index of the content part within the item |
previous_item_id | string | ID of the preceding item, used to preserve ordering |
reason | string | Present in cancelled events (e.g., "turn_detected") |
error | object | Present in failed events |
tokens | number | Token count for the response (assistant events) |
is_forced | boolean | true when the message was injected by the server rather than produced by user turn-taking |
timestamp | datetime | When the event occurred (included in every message) |
The first message_received frame of a session is a session marker: type: "session" with no role, action, or content. Skip any frame where data.message.type === "session" before processing.
The examples below show the contents of the message object — i.e. what you find at data.message.
User message lifecycle
These events track the user's speech input and transcription, all with role: "user".
speech_started
The user began speaking. The item_id identifies the user message item that will be created when speech stops.
{
"role": "user",
"action": "speech_started",
"item_id": "item_APuyFRtVtA9m8o9Qhyj9e"
}speech_stopped
The user stopped speaking.
{
"role": "user",
"action": "speech_stopped",
"item_id": "item_APuyFRtVtA9m8o9Qhyj9e"
}created
The user's speech transcription has started. The previous_item_id links this item to the preceding item in the conversation.
{
"role": "user",
"action": "created",
"item_id": "item_APuyFRtVtA9m8o9Qhyj9e",
"previous_item_id": "item_APuxlnjrohdgargU6tHe9"
}completed
The user's speech transcription is finalized. The content field contains the full transcription text.
{
"role": "user",
"action": "completed",
"item_id": "item_APuyFRtVtA9m8o9Qhyj9e",
"content": "Hi there!"
}This event can arrive after assistant response events have already started. Do not assume user transcription completes before the assistant begins responding.
If content is empty, the user's speech could not be recognized. You may want to display a placeholder like "[inaudible]" in your UI.
failed
The user's speech transcription failed. The error field contains details.
{
"role": "user",
"action": "failed",
"item_id": "item_APuyFRtVtA9m8o9Qhyj9e",
"error": { }
}Assistant response lifecycle
These events track the agent's response generation, all with role: "assistant".
created (response-level)
A new response has started generating. The response_id groups all subsequent events for this response.
{
"role": "assistant",
"action": "created",
"response_id": "resp_APuyF0H6ffvKBP0MJE6h9"
}created (item linked to response)
A response item has been linked to the response.
{
"role": "assistant",
"action": "created",
"response_id": "resp_APuyF0H6ffvKBP0MJE6h9",
"item_id": "item_APuy7jTGdjgsW0gOGOko2"
}created (item-level)
The response item has started generating. The previous_item_id preserves ordering in the conversation.
{
"role": "assistant",
"action": "created",
"item_id": "item_APuy7jTGdjgsW0gOGOko2",
"previous_item_id": "item_APuyFRtVtA9m8o9Qhyj9e"
}delta
An incremental text chunk of the response. These arrive in rapid succession as the agent generates its reply.
{
"role": "assistant",
"action": "delta",
"response_id": "resp_APuyF0H6ffvKBP0MJE6h9",
"item_id": "item_APuy7jTGdjgsW0gOGOko2",
"content": "Hello! How can I"
}completed
The response item is finished. The content field contains the full response text.
{
"role": "assistant",
"action": "completed",
"response_id": "resp_APuyF0H6ffvKBP0MJE6h9",
"item_id": "item_APuy7jTGdjgsW0gOGOko2",
"content": "Hello! How can I assist you today?"
}interrupted
The user barged in while the agent was responding. This fires on the user side with role: "user".
{
"role": "user",
"action": "interrupted",
"item_id": "item_APuyFRtVtA9m8o9Qhyj9e"
}cancelled
The response was cancelled. The reason field explains why (e.g., "turn_detected" when the user started speaking).
{
"role": "assistant",
"action": "cancelled",
"response_id": "resp_APuyF0H6ffvKBP0MJE6h9",
"reason": "turn_detected"
}failed
The response generation failed. The error field contains details.
{
"role": "assistant",
"action": "failed",
"response_id": "resp_APuyF0H6ffvKBP0MJE6h9",
"error": { }
}MCP tool lifecycle
These events report what the agent is doing with an attached MCP server. They have no role — instead, type tells you which stage you are looking at.
Here is the order they arrive in during a session:
- The session starts. You receive one
mcp_toolsmessage for each attached server, listing the tools that server exposes — or reporting that it could not be reached. - The agent decides to call a tool. You receive an
mcp_callmessage withaction: "created", naming the tool and the arguments it will use. - The call finishes. You receive a second
mcp_callmessage, sameitem_id, withaction: "completed"or"failed".
Steps 2 and 3 repeat for every tool call the agent makes.
If the server was registered with requireApproval: "always", one more message arrives between steps 2 and 3: an mcp_approval_request. The call does not run until you answer it, and if you never do, it is dropped after 15 seconds.
mcp_tools event
Tool discovery, sent once per attached server as the session starts.
{
"type": "mcp_tools",
"action": "completed",
"server": "gmail",
"tool_names": ["search_emails", "get_recent_emails"]
}| Field | Type | Description |
|---|---|---|
action | string | completed or failed — see below |
server | string | ID of the MCP server this message is about, as you attached it (for example gmail) |
tool_names | array of strings | The tools this server exposes, for example ["search_emails", "read_email"]. Present on completed; an empty array means the server exposes nothing |
error | object | Why the server could not be reached. Present on failed |
Actions
completed— the server was reached and its tools listed. The agent can now call anything intool_names.failed— the server could not be reached. The agent starts with no tools from it and will not recover during this session. This is your earliest warning of a bad URL, an expired credential or a missing token.
mcp_call event
A tool invocation. You get one message when the call starts and another when it resolves.
{
"type": "mcp_call",
"action": "created",
"item_id": "mcp_a1b2c3",
"server": "gmail",
"tool": "get_recent_emails",
"content": "{\"top_k\": 3}"
}| Field | Type | Description |
|---|---|---|
action | string | created, completed or failed — see below |
item_id | string | Identifies this call, prefixed mcp_. The same value appears on the start and the resolution, so use it to pair them |
server | string | ID of the MCP server providing the tool |
tool | string | Name of the tool being called, for example get_recent_emails |
content | string | The call's arguments as a JSON string on created, and the tool's output on completed |
error | object | Details of the failure. Present on failed |
Actions
created— the agent invoked a tool.contentholds the arguments it chose. The call has not returned yet, and on an approval-gated server it has not even run.completed— the call returned.contentholds the output the agent will speak from.failed— the call errored, typically a rejection from the MCP server itself such as an expired token. The agent says nothing about it.
mcp_approval_request event
A tool call is waiting for your decision. Answer it with send_mcp_approval.
{
"type": "mcp_approval_request",
"action": "created",
"item_id": "mcpr_x9y8z7",
"server": "gmail",
"tool": "get_recent_emails",
"content": "{\"top_k\": 3}"
}| Field | Type | Description |
|---|---|---|
action | string | created, completed or cancelled — see below |
item_id | string | The approval request ID, prefixed mcpr_. Present on every action. Send it back as approval_request_id |
server | string | ID of the MCP server requesting the call. Present on created and cancelled |
tool | string | Name of the tool awaiting approval — show this to the user. Present on created and cancelled |
content | string | The arguments the tool would run with, as a JSON string. Show these too: they are what the user is approving. Present on created |
result | string | approved or rejected. Present on completed |
source | string | How the decision was made. client when your app answered with send_mcp_approval. Present on completed |
Actions
created— approval is needed. Present the choice now; the 15-second clock is already running.completed— somebody answered.resulttells you which way. Stop showing the prompt.cancelled— nobody answered within 15 seconds, so the call was rejected on your behalf and will not run. Disable the approval controls.
completed and cancelled are mutually exclusive. A request that times out is reported as cancelled only — you never receive a completed for it. So completed always means a real decision was made, and cancelled always means nobody made one.
You have 15 seconds to answer before the request is rejected automatically. Show the choice as soon as created arrives, and disable the controls on cancelled.
ui_update
Unlike the other events, ui_update doesn't originate from the platform — it's sent by your own functions server over the WebSocket that a WebSocket-based explicit tool keeps open during the session. The platform relays it to the client without validating or modifying the payload.
{
"event": "ui_update",
"data": { "view": "booking-confirmation", "bookingId": "bk_123" }
}data is exactly what your server sent — any JSON, any shape. Use it to drive UI from your backend in sync with the conversation: show a confirmation card when a booking tool completes, stream progress for a long-running action, or update page state the agent just changed.
Because the payload is entirely yours, dispatch on a field you define (for example a data.view or data.kind discriminator) rather than assuming a single shape:
onData: (data) => {
if (data.event === "ui_update") {
renderFromServer(data.data);
}
}Delivery notes:
- One-way, fire-and-forget: no acknowledgement, and updates are not stored — a client that connects later won't receive past updates. Re-send current state if the UI must survive reconnects.
- Your server can send it at any time while its WebSocket is connected — it is not tied to a tool call.
- Requires at least one WebSocket-based explicit tool on the session; HTTP-based tools have no persistent connection to send from.