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
| Event | Payload type | Description |
|---|
loading | DailyEventObjectNoPayload | The Daily call bundle has started loading (iframe mode). |
load-attempt-failed | DailyEventObjectGenericError | The call bundle failed to load. errorMsg has details. |
loaded | DailyEventObjectNoPayload | The call bundle finished loading. |
started-camera | — | Camera/mic acquisition completed (before joining). |
camera-error | DailyEventObjectCameraError | Camera or mic acquisition failed. |
joining-meeting | DailyEventObjectNoPayload | join() was called; connection in progress. |
joined-meeting | DailyEventObjectParticipants | Successfully joined. participants contains the full initial snapshot. |
left-meeting | DailyEventObjectNoPayload | leave() or ejection completed. |
call-instance-destroyed | DailyEventObjectNoPayload | destroy() was called. |
access-state-updated | DailyEventObjectAccessState | Room 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
| Event | Payload type | Description |
|---|
participant-joined | DailyEventObjectParticipant | A remote participant entered the room. |
participant-updated | DailyEventObjectParticipant | Any field on a participant changed. |
participant-left | DailyEventObjectParticipantLeft | A remote participant left or was ejected. reason is 'hidden' if they remain on the call but no longer have presence. |
participant-counts-updated | DailyEventObjectParticipantCounts | Aggregate present/hidden counts changed. |
active-speaker-change | DailyEventObjectActiveSpeakerChange | The dominant speaker changed. activeSpeaker.peerId is the new speaker’s session_id. |
waiting-participant-added | DailyEventObjectWaitingParticipant | A participant entered the knock-to-join lobby. |
waiting-participant-updated | DailyEventObjectWaitingParticipant | A waiting participant’s state changed. |
waiting-participant-removed | DailyEventObjectWaitingParticipant | A 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);
});
| Event | Payload type | Description |
|---|
track-started | DailyEventObjectTrack | A track became playable. type is 'video', 'audio', 'screenVideo', 'screenAudio', or a custom name. |
track-stopped | DailyEventObjectTrack | A track left playable state. |
local-screen-share-started | — | The local participant’s screen share started. |
local-screen-share-stopped | — | The local screen share stopped normally. |
local-screen-share-canceled | — | The user dismissed the browser’s screen picker. |
local-audio-level | DailyEventObjectLocalAudioLevel | Local audio level update. audioLevel is 0.0–1.0. Requires startLocalAudioLevelObserver(). |
remote-participants-audio-level | DailyEventObjectRemoteParticipantsAudioLevel | Map of session_id → audioLevel for remote participants. Requires startRemoteParticipantsAudioLevelObserver(). |
face-counts-updated | DailyEventObjectFaceCounts | Number 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
| Event | Payload type | Description |
|---|
recording-started | DailyEventObjectRecordingStarted | A recording started. local is true for browser-based recording. |
recording-stopped | DailyEventObjectRecordingStopped | A recording stopped. |
recording-error | DailyEventObjectRecordingError | Recording 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
| Event | Payload type | Description |
|---|
live-streaming-started | DailyEventObjectLiveStreamingStarted | Live stream started. |
live-streaming-updated | DailyEventObjectLiveStreamingUpdated | Stream state or endpoint changed. state is 'connected' or 'interrupted'. |
live-streaming-stopped | DailyEventObjectLiveStreamingStopped | Live stream stopped. |
live-streaming-error | DailyEventObjectLiveStreamingError | Live streaming error. |
Transcription Events
Paid plans only
| Event | Payload type | Description |
|---|
transcription-started | DailyEventObjectTranscriptionStarted | Transcription started. Contains model, language, and configuration details. |
transcription-stopped | DailyEventObjectTranscriptionStopped | Transcription stopped. |
transcription-error | DailyEventObjectTranscriptionError | Transcription error. |
transcription-message | DailyEventObjectTranscriptionMessage | A transcription result. text is the transcript; participantId identifies the speaker. |
call.on('transcription-message', ({ participantId, text, timestamp }) => {
addCaption({ participantId, text, timestamp });
});
Network Events
| Event | Payload type | Description |
|---|
network-quality-change | DailyEventObjectNetworkQualityEvent | Network quality changed. networkState is 'good', 'warning', 'bad', or 'unknown'. |
network-connection | DailyEventObjectNetworkConnectionEvent | Low-level connection event. type is 'signaling', 'peer-to-peer', or 'sfu'. |
cpu-load-change | DailyEventObjectCpuLoadEvent | CPU load state changed. cpuLoadState is 'low' or 'high'. |
test-completed | DailyEventObjectTestCompleted | A quality or connectivity test finished. |
call.on('network-quality-change', ({ networkState, networkStateReasons }) => {
if (networkState === 'bad') {
showNetworkWarning(networkStateReasons);
}
});
Settings & Device Events
| Event | Payload type | Description |
|---|
available-devices-updated | DailyEventObjectAvailableDevicesUpdated | The list of available input/output devices changed. |
selected-devices-updated | DailyEventObjectSelectedDevicesUpdated | The active camera, mic, or speaker selection changed. |
input-settings-updated | DailyEventObjectInputSettingsUpdated | Input settings (noise cancellation, background blur, etc.) changed. |
send-settings-updated | DailyEventObjectSendSettingsUpdated | Video send settings (bitrate, simulcast) changed. |
receive-settings-updated | DailyEventObjectReceiveSettingsUpdated | Receive settings (simulcast layer) changed (call object mode). |
Messaging Events
| Event | Payload type | Description |
|---|
app-message | DailyEventObjectAppMessage | A 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-updated | DailyEventObjectMeetingSessionSummaryUpdated | The meeting session summary changed. |
meeting-session-state-updated | DailyEventObjectMeetingSessionStateUpdated | Shared 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
| Event | Payload type | Description |
|---|
dialin-ready | DailyEventObjectDialinReady | SIP endpoint is ready to accept dial-in calls. sipEndpoint is the address. |
dialin-connected | DailyEventObjectDialinConnected | A dial-in call connected. |
dialin-error | DailyEventObjectDialinError | Dial-in error. |
dialin-stopped | DailyEventObjectDialinStopped | Dial-in call stopped. |
dialin-warning | DailyEventObjectDialinWarning | Non-fatal dial-in warning. |
dialout-connected | DailyEventObjectDialOutConnected | An outbound dial-out call connected. |
dialout-answered | DailyEventObjectDialOutAnswered | An outbound call was answered. |
dialout-error | DailyEventObjectDialOutError | Dial-out error. |
dialout-stopped | DailyEventObjectDialOutStopped | Dial-out call stopped. |
dialout-warning | DailyEventObjectDialOutWarning | Non-fatal dial-out warning. |
Error Events
| Event | Payload type | Description |
|---|
error | DailyEventObjectFatalError | A fatal error occurred. error.type is one of DailyFatalErrorType. The call ends automatically. |
nonfatal-error | DailyEventObjectNonFatalError | A 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:
| Event | Description |
|---|
fullscreen | The iframe entered fullscreen. |
exited-fullscreen | The iframe exited fullscreen. |
pip-started | Picture-in-picture started. |
pip-stopped | Picture-in-picture stopped. |
sidebar-view-changed | The active sidebar panel changed. |
custom-button-click | A custom tray button was clicked. |
active-speaker-mode-change | Active speaker mode was toggled. |
show-local-video-changed | Whether the local video tile is visible changed. |
lang-updated | The UI language changed. |
theme-updated | The 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.