Roxels/ docs
embed

Events

Every event the embed can emit. Subscribe via the controller (call.on(event, cb)) or globally (Roxels.on(event, cb)). Events fire identically across modal, compact, and headless modes — the same event stream powers all three.

For narrative usage in headless, see Headless mode.

Lifecycle events

These describe the connection state of the call.

session

Fired once after start(), when the session id is known.

call.on("session", ({ sessionId }) => {
  console.log("session is", sessionId);
});

Payload:

{
  sessionId: string;
}

status

Fired on every connection-status change.

call.on("status", ({ status }) => {
  // status: prewarm | loading | ready | joining | connected | disconnecting | ended | error
});

Payload:

{
  status: "prewarm" |
    "loading" |
    "ready" |
    "joining" |
    "connected" |
    "disconnecting" |
    "ended" |
    "error";
}

connected

Fired once, the first time the call enters connected. After this, voice and chat events start flowing.

call.on("connected", () => console.log("the call is live"));

Payload: none.

ending

Fired when the call is gracefully closing (after hangUp() or when the agent decides the conversation is done).

Payload: none.

ended

Fired when the call has finished closing. The iframe is still mounted; cleanup is in progress.

Payload: none.

complete

Fired once the conversation has finished and final results are ready. The most useful "the call is done" hook.

call.on("complete", (results) => {
  console.log("Summary:", results.summary);
  console.log("Findings:", results.findings);
});

Payload:

{
  summary: string,                // human-readable summary
  findings: object,               // structured data from all committed goals
  duration_seconds: number,
  external_id: string,
  auto_close: boolean             // template setting — should your UI auto-dismiss?
}

error

Fired on any call-level error.

call.on("error", ({ error }) => {
  if (error === "microphone_blocked") showMicHelp();
});

Payload:

{
  error: string;
}

Common error codes:

Code Cause
microphone_blocked Browser denied mic access; check host's Permissions-Policy.
session_create_failed Invalid embed key, domain not allowlisted, or server error.
connection_failed Couldn't reach the LiveKit room.
session_ended_unexpectedly The session ended outside the normal complete path.

Voice events

voice_state

The agent's voice activity changed.

call.on("voice_state", ({ state }) => {
  // state: idle | listening | thinking | speaking
  updateOrb(state);
});

Payload:

{
  state: "idle" | "listening" | "thinking" | "speaking";
}

Use this to drive an orb, indicator, or animation in headless UIs.

Chat events

chat

A chat message — either direction.

call.on("chat", ({ id, role, text, timestamp }) => {
  appendToTranscript({ role, text, timestamp });
});

Payload:

{
  id: string,
  role: "user" | "assistant",
  text: string,
  timestamp: string   // ISO-8601
}

Extraction and goal events

understanding

The agent extracted structured data, or the working understanding of a goal updated.

call.on("understanding", (event) => {
  if (event.data) {
    console.log("captured:", event.goal_id, event.data);
  } else if (event.text) {
    console.log("free-form note:", event.text);
  }
});

Payload (one of):

// Structured extraction (when a goal has a schema)
{
  data: object,            // matches the goal's schema
  goal_id: string,
  cumulative: object,      // all data captured so far for this goal
  source: "extraction" | "tool" | "user",
  timestamp: string
}
 
// Unstructured note (free-form text)
{
  text: string,
  goal_id: string,
  timestamp: string
}

goal_transition

A goal committed, or the active goal changed.

call.on("goal_transition", ({ from_goal, to_goal, timestamp }) => {
  highlightStep(to_goal);
});

Payload:

{
  from_goal: string | null,   // null if this is the first goal
  to_goal: string,
  timestamp: string
}

Document / form events

These fire when the template uses the live document or form view.

document

The current document state updated.

call.on("document", (state) => renderDocument(state));

Payload: the current document state (template-specific shape).

document_clear

A document was cleared.

call.on("document_clear", ({ doc_id }) => clearDocument(doc_id));

Payload:

{
  doc_id: string;
}

Reflecting-state events

These fire whenever the relevant state changes, regardless of who changed it (your code, the user, the agent).

mic_state

Mic enabled/disabled changed.

call.on("mic_state", ({ enabled }) => setMicButtonState(enabled));

Payload: { enabled: boolean }

screenshare_state

Screen-share active/inactive changed.

Payload: { active: boolean }

paused / resumed / paused_ended

The pause state changed.

call.on("paused", () => showPausedOverlay());
call.on("resumed", () => hidePausedOverlay());
call.on("paused_ended", () => {
  /* call ended while paused */
});

Payload: none.

file_uploaded

A file you uploaded finished processing on the agent side.

call.on("file_uploaded", ({ id, filename }) => {
  markUploadDone(id, filename);
});

Payload: { id: string, filename: string }

command_ack

The iframe acknowledged a command. Useful for debugging command-queue behavior.

Payload: { command: string }

Subscribing patterns

Bind early

You can (and should) bind events before await call.ready. Events that fire during connection (session, status transitions) won't be missed.

const call = Roxels.start({ display: "none", templateKey: "rk-..." });
call.on("status", handleStatus);
call.on("error", handleError);
await call.ready;

Bind multiple handlers

Multiple handlers for the same event are fine; they all fire.

call.on("complete", saveToBackend);
call.on("complete", dismissUI);
call.on("complete", trackAnalytics);

Unsubscribe

Always unsubscribe in cleanup:

const onChat = (msg) => {
  /* ... */
};
call.on("chat", onChat);
// later:
call.off("chat", onChat);

call.close() removes all listeners automatically, so this is mostly relevant for long-lived calls where you want to detach a particular handler.

Webhooks vs JS events

Three of these events have webhook equivalents that fire from the Roxels backend to your server:

JS event Webhook equivalent When you'd use webhooks
understanding The same data is fired to your webhook (if configured per goal). When you want backend records, idempotency, retries.
goal_transition Output webhooks fire on goal commit. When backend systems must act on goal completion.
complete The full result lands at your conversation-completion webhook. Canonical record of the conversation outcome.

For backend-critical work, prefer webhooks. They retry, they're idempotent, they survive a closed browser tab. See Webhooks overview.