Events

Listen to conversation and speech events from an Experience.

Overview

Register handlers with experience.on(event, handler) after Experience.startSession(). Component integrations use the matching callback props (onMessage, onActivityChange, …).

Startup

ready means the player visuals are mounted. started means the player-owned start button was used and audio is unlocked.

const experience = await Experience.startSession({
  experienceId: 'exp_…'
});

experience.on('ready', (evt) => {
  // evt: ExperienceEventEnvelope<{ session }>
  console.log('Player visuals ready', evt.data.session.id);
});

experience.on('started', (evt) => {
  // evt.data.mode — experience mode after audio unlock
  console.log('Audio and session started in', evt.data.mode, 'mode');
});

await experience.attach({ container });

ready includes the public session facts (not a parseable launch). started includes the experience mode. Both replay asynchronously for handlers registered after the event. The onStart option on Experience.startSession() is a convenience callback for startup completion.

Speech and transcripts

Emitted during speak(), listening, and custom processor conversation. Partial transcript revisions include monotonic revision and optional delta. Only updates with isFinal: true commit a user message to conversation history.

experience.on('userTranscript', (evt) => {
  const update = evt.data;
  if (!update.isFinal) {
    liveCaptionEl.textContent = update.text;
    return;
  }
  liveCaptionEl.textContent = update.text;
  commitUtterance(update.utteranceId, update.text);
});

experience.on('userSpeechStarted', () => micIndicator.classList.add('active'));
experience.on('userSpeechEnded', () => micIndicator.classList.remove('active'));
experience.on('characterSpeechStarted', (evt) => {
  console.log('Speaking', evt.data.characterId, evt.data.text, evt.data.source);
});

experience.on('characterSpeechEnded', (evt) => {
  console.log('Speech ended', evt.data.reason, evt.data.durationMs);
});

experience.on('conversationUpdate', (evt) => {
  console.log('History length', evt.data.length);
});

experience.on('listeningState', (evt) => {
  console.log('Mic gate', evt.data);
});

Custom conversation processor

When you supply conversationProcessor on startSession(), processor failures surface as conversationProcessorError. The managed LLM is not used as a fallback.

experience.on('conversationProcessorError', (evt) => {
  console.error('Processor failed for', evt.data.utteranceId, evt.data.message);
});

Conversation

// Every experience.on handler receives an envelope:
// { id: 'evt_…', type, sessionId, timestamp, data }

experience.on('message', (evt) => {
  // evt.data.status is always 'final' for committed history messages
  console.log(evt.data.role, evt.data.text, evt.data.source, evt.data.status);
});

experience.on('activityChange', (evt) => {
  // evt.data: 'idle' | 'listening' | 'thinking' | 'speaking'
  console.log('Activity', evt.data);
});

Lip-sync and facial animation run inside the hosted player. Integrators do not receive viseme or animation keyframe events — use characterSpeechStarted / characterSpeechEnded to sync host UI with speech.

Player attach callbacks

Embed lifecycle is on the object returned from attach() (and on component props), not on experience.on():

const player = await experience.attach({
  container,
  onPlayerStatusChange: (status) => {
    // PlayerStatus: loading | scene | ready | error
    console.log(status);
  }
});

player.on('close', ({ reason, returnUrl }) => {
  console.log('Player closed', reason, returnUrl);
});

Event reference

EventPayloadDescription
readyenvelope .data: { session }Player visuals are mounted and ready
startedenvelope .data: { mode }Player startup click, audio unlock, and session startup completed
userTranscriptenvelope .data: { utteranceId, text, revision, isFinal, delta? }Partial and final STT updates. Ephemeral until isFinal: true.
userSpeechStartedenvelope .data: { utteranceId? }VAD detected speech activity (auto mode telemetry)
userSpeechEndedenvelope .data: { utteranceId? }VAD detected end of speech; final text may arrive after this event
characterSpeechStartedenvelope .data: { speechId, turnId, characterId, text, source }Character began speaking (speak, opening, or llm)
characterSpeechEndedenvelope .data: { speechId, turnId, characterId, text, source, durationMs?, reason }Character speech finished or was interrupted
characterFocusChangedenvelope .data: { characterId, reason }Presentation focus changed on a multi-Character node (reason: speech | api | node_enter). Not emitted on single-Character nodes.
conversationUpdateenvelope .data: ConversationMessage[]Immutable snapshot of in-session conversation history
listeningStateenvelope .data: booleanManual listening gate opened (true) or closed (false)
messageenvelope .data: ConversationMessageDurable user or assistant message (status: 'final', plus source, role, text, ids)
activityChangeenvelope .data: 'idle' | 'listening' | 'speaking' | 'thinking'Session listening / speaking activity for UI sync
interactionChangeenvelope .data: ExperienceInteractionCaptions, text/mic allow + enabled state, mute character speech, or Edit (allowRewind) changed — from the player or after setInteraction()
feedbackenvelope .data: { contractVersion: 'feedback.v1', types: [...] }End-of-session Feedback scores (when Feedback is attached and scoring succeeds). Always emitted when available — even if the player also shows Feedback UI.
feedbackFailedenvelope .data: { error, model? }End-of-session Feedback scoring failed
conversationProcessorErrorenvelope .data: { utteranceId, message }Browser conversationProcessor threw or rejected

Not on experience.on()

SignalHow to listen
Player embed stateattach({ onPlayerStatusChange }) or component onPlayerStatusChange
Player closeplayer.on('close', …) after attach(), or component onClose