Skip to main content
Daily uses a typed event system built on Node.js EventEmitter. Every state change — participant joins, track goes live, recording starts, network degrades — is surfaced as a typed event object.

Subscribing to Events

// Subscribe — fires on every occurrence
call.on(event, handler);

// Subscribe once — auto-removes after first invocation
call.once(event, handler);

// Unsubscribe
call.off(event, handler);
All three methods return the DailyCall instance for chaining.

Base Event Shape

Every event object extends DailyEventObjectBase:
type DailyEventObjectBase = {
  action: DailyEvent;   // the event name string
  callClientId: string; // ID of the DailyIframe instance that emitted it
};

TypeScript Type Narrowing

The DailyEventObject<T> generic type maps event name strings to their specific payload types. TypeScript will narrow the handler argument automatically:
import type { DailyEventObject } from '@daily-co/daily-js';

// TypeScript knows `event` is DailyEventObjectParticipant
call.on('participant-joined', (event: DailyEventObject<'participant-joined'>) => {
  const { participant } = event; // fully typed DailyParticipant
  console.log(participant.user_name);
});

// TypeScript knows `event` is DailyEventObjectTrack
call.on('track-started', (event: DailyEventObject<'track-started'>) => {
  const { track, type, participant } = event;
  console.log(`${type} track started by ${participant?.user_name}`);
});

// TypeScript knows `event` is DailyEventObjectFatalError
call.on('error', (event: DailyEventObject<'error'>) => {
  console.error(event.errorMsg, event.error);
});

Event Reference

Lifecycle Events

EventPayload typeDescription
loadingDailyEventObjectNoPayloadThe Daily call bundle has started loading (iframe mode).
load-attempt-failedDailyEventObjectGenericErrorThe call bundle failed to load. errorMsg has details.
loadedDailyEventObjectNoPayloadThe call bundle finished loading.
started-cameraCamera/mic acquisition completed (before joining).
camera-errorDailyEventObjectCameraErrorCamera or mic acquisition failed.
joining-meetingDailyEventObjectNoPayloadjoin() was called; connection in progress.
joined-meetingDailyEventObjectParticipantsSuccessfully joined. participants contains the full initial snapshot.
left-meetingDailyEventObjectNoPayloadleave() or ejection completed.
call-instance-destroyedDailyEventObjectNoPayloaddestroy() was called.
access-state-updatedDailyEventObjectAccessStateRoom access level changed (e.g. knock-to-join lobby admission).
call.on('joined-meeting', ({ participants }) => {
  console.log('All participants at join time:', participants);
});

call.on('left-meeting', () => {
  console.log('Left the call');
});

call.on('camera-error', ({ error }) => {
  if (error.type === 'permissions') {
    console.error('Camera/mic permissions denied');
  }
});

Participant Events

EventPayload typeDescription
participant-joinedDailyEventObjectParticipantA remote participant entered the room.
participant-updatedDailyEventObjectParticipantAny field on a participant changed.
participant-leftDailyEventObjectParticipantLeftA remote participant left or was ejected. reason is 'hidden' if they remain on the call but no longer have presence.
participant-counts-updatedDailyEventObjectParticipantCountsAggregate present/hidden counts changed.
active-speaker-changeDailyEventObjectActiveSpeakerChangeThe dominant speaker changed. activeSpeaker.peerId is the new speaker’s session_id.
waiting-participant-addedDailyEventObjectWaitingParticipantA participant entered the knock-to-join lobby.
waiting-participant-updatedDailyEventObjectWaitingParticipantA waiting participant’s state changed.
waiting-participant-removedDailyEventObjectWaitingParticipantA waiting participant was admitted, denied, or left.
call.on('active-speaker-change', ({ activeSpeaker }) => {
  highlightTile(activeSpeaker.peerId);
});

call.on('waiting-participant-added', ({ participant }) => {
  showKnockNotification(participant.name);
});

Media Events

EventPayload typeDescription
track-startedDailyEventObjectTrackA track became playable. type is 'video', 'audio', 'screenVideo', 'screenAudio', or a custom name.
track-stoppedDailyEventObjectTrackA track left playable state.
local-screen-share-startedThe local participant’s screen share started.
local-screen-share-stoppedThe local screen share stopped normally.
local-screen-share-canceledThe user dismissed the browser’s screen picker.
local-audio-levelDailyEventObjectLocalAudioLevelLocal audio level update. audioLevel is 0.0–1.0. Requires startLocalAudioLevelObserver().
remote-participants-audio-levelDailyEventObjectRemoteParticipantsAudioLevelMap of session_id → audioLevel for remote participants. Requires startRemoteParticipantsAudioLevelObserver().
face-counts-updatedDailyEventObjectFaceCountsNumber of faces detected in local video changed.
const AUDIO_LEVEL_THRESHOLD = 0.05;

await call.startLocalAudioLevelObserver(100); // poll every 100ms

call.on('local-audio-level', ({ audioLevel }) => {
  setIsSpeaking(audioLevel > AUDIO_LEVEL_THRESHOLD);
});

Recording Events

Paid plans only
EventPayload typeDescription
recording-startedDailyEventObjectRecordingStartedA recording started. local is true for browser-based recording.
recording-stoppedDailyEventObjectRecordingStoppedA recording stopped.
recording-errorDailyEventObjectRecordingErrorRecording encountered an error.
call.on('recording-started', ({ local, recordingId, type }) => {
  console.log(`Recording started: type=${type}, local=${local}, id=${recordingId}`);
});

call.on('recording-data', ({ data, finished }) => {
  recordingChunks.push(data);
  if (finished) {
    const blob = new Blob(recordingChunks, { type: 'video/mp4' });
    saveRecording(blob);
  }
});

Live Streaming Events

Paid plans only
EventPayload typeDescription
live-streaming-startedDailyEventObjectLiveStreamingStartedLive stream started.
live-streaming-updatedDailyEventObjectLiveStreamingUpdatedStream state or endpoint changed. state is 'connected' or 'interrupted'.
live-streaming-stoppedDailyEventObjectLiveStreamingStoppedLive stream stopped.
live-streaming-errorDailyEventObjectLiveStreamingErrorLive streaming error.

Transcription Events

Paid plans only
EventPayload typeDescription
transcription-startedDailyEventObjectTranscriptionStartedTranscription started. Contains model, language, and configuration details.
transcription-stoppedDailyEventObjectTranscriptionStoppedTranscription stopped.
transcription-errorDailyEventObjectTranscriptionErrorTranscription error.
transcription-messageDailyEventObjectTranscriptionMessageA transcription result. text is the transcript; participantId identifies the speaker.
call.on('transcription-message', ({ participantId, text, timestamp }) => {
  addCaption({ participantId, text, timestamp });
});

Network Events

EventPayload typeDescription
network-quality-changeDailyEventObjectNetworkQualityEventNetwork quality changed. networkState is 'good', 'warning', 'bad', or 'unknown'.
network-connectionDailyEventObjectNetworkConnectionEventLow-level connection event. type is 'signaling', 'peer-to-peer', or 'sfu'.
cpu-load-changeDailyEventObjectCpuLoadEventCPU load state changed. cpuLoadState is 'low' or 'high'.
test-completedDailyEventObjectTestCompletedA quality or connectivity test finished.
call.on('network-quality-change', ({ networkState, networkStateReasons }) => {
  if (networkState === 'bad') {
    showNetworkWarning(networkStateReasons);
  }
});

Settings & Device Events

EventPayload typeDescription
available-devices-updatedDailyEventObjectAvailableDevicesUpdatedThe list of available input/output devices changed.
selected-devices-updatedDailyEventObjectSelectedDevicesUpdatedThe active camera, mic, or speaker selection changed.
input-settings-updatedDailyEventObjectInputSettingsUpdatedInput settings (noise cancellation, background blur, etc.) changed.
send-settings-updatedDailyEventObjectSendSettingsUpdatedVideo send settings (bitrate, simulcast) changed.
receive-settings-updatedDailyEventObjectReceiveSettingsUpdatedReceive settings (simulcast layer) changed (call object mode).

Messaging Events

EventPayload typeDescription
app-messageDailyEventObjectAppMessageA message sent via sendAppMessage(). data is the payload; fromId is the sender’s session_id.
meeting-session-updated(deprecated)Use meeting-session-summary-updated instead.
meeting-session-summary-updatedDailyEventObjectMeetingSessionSummaryUpdatedThe meeting session summary changed.
meeting-session-state-updatedDailyEventObjectMeetingSessionStateUpdatedShared meeting session state changed.
// Broadcast to all participants
call.sendAppMessage({ type: 'reaction', emoji: '👋' }, '*');

// Receive messages
call.on('app-message', ({ data, fromId }) => {
  console.log(`Message from ${fromId}:`, data);
});

Telephony Events (SIP/PSTN)

Paid plans only
EventPayload typeDescription
dialin-readyDailyEventObjectDialinReadySIP endpoint is ready to accept dial-in calls. sipEndpoint is the address.
dialin-connectedDailyEventObjectDialinConnectedA dial-in call connected.
dialin-errorDailyEventObjectDialinErrorDial-in error.
dialin-stoppedDailyEventObjectDialinStoppedDial-in call stopped.
dialin-warningDailyEventObjectDialinWarningNon-fatal dial-in warning.
dialout-connectedDailyEventObjectDialOutConnectedAn outbound dial-out call connected.
dialout-answeredDailyEventObjectDialOutAnsweredAn outbound call was answered.
dialout-errorDailyEventObjectDialOutErrorDial-out error.
dialout-stoppedDailyEventObjectDialOutStoppedDial-out call stopped.
dialout-warningDailyEventObjectDialOutWarningNon-fatal dial-out warning.

Error Events

EventPayload typeDescription
errorDailyEventObjectFatalErrorA fatal error occurred. error.type is one of DailyFatalErrorType. The call ends automatically.
nonfatal-errorDailyEventObjectNonFatalErrorA recoverable error. type is one of DailyNonFatalErrorType. The call continues.
call.on('error', ({ error, errorMsg }) => {
  switch (error?.type) {
    case 'ejected':
      showMessage('You were removed from the call.');
      break;
    case 'meeting-full':
      showMessage('The room is at capacity.');
      break;
    case 'exp-room':
    case 'exp-token':
      showMessage('This room or your access token has expired.');
      break;
    case 'connection-error':
      showMessage('Connection failed. Please check your network.');
      break;
    default:
      console.error('Fatal error:', errorMsg);
  }
});

call.on('nonfatal-error', ({ type, errorMsg, details }) => {
  console.warn(`Non-fatal error [${type}]:`, errorMsg, details);
});

Iframe UI Events

These events are only emitted in iframe mode:
EventDescription
fullscreenThe iframe entered fullscreen.
exited-fullscreenThe iframe exited fullscreen.
pip-startedPicture-in-picture started.
pip-stoppedPicture-in-picture stopped.
sidebar-view-changedThe active sidebar panel changed.
custom-button-clickA custom tray button was clicked.
active-speaker-mode-changeActive speaker mode was toggled.
show-local-video-changedWhether the local video tile is visible changed.
lang-updatedThe UI language changed.
theme-updatedThe UI theme changed.

Removing Listeners

Use call.off() to remove a specific listener:
// Remove a specific listener
call.off('participant-joined', myHandler);
DailyCall (which extends Node.js EventEmitter at runtime) also inherits removeAllListeners(), though this method is not part of the typed DailyCall interface:
// Remove all listeners for one event (EventEmitter runtime method)
(call as any).removeAllListeners('participant-joined');

// Remove all listeners for all events (EventEmitter runtime method)
(call as any).removeAllListeners();
Always remove event listeners before calling call.destroy() to prevent memory leaks, especially in single-page applications that re-render frequently.