> ## Documentation Index
> Fetch the complete documentation index at: https://docs.daily.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Subscribe to Daily call events using on(), once(), and off() with full TypeScript type narrowing.

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

```typescript theme={null}
// 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`:

```typescript theme={null}
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:

```typescript theme={null}
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`](/reference/daily-js/events/lifecycle-events#loading) | `DailyEventObjectNoPayload` | The Daily call bundle has started loading (iframe mode). |
| [`load-attempt-failed`](/reference/daily-js/events/lifecycle-events#load-attempt-failed) | `DailyEventObjectGenericError` | The call bundle failed to load. `errorMsg` has details. |
| [`loaded`](/reference/daily-js/events/lifecycle-events#loaded) | `DailyEventObjectNoPayload` | The call bundle finished loading. |
| [`started-camera`](/reference/daily-js/events/settings-events#started-camera) | — | Camera/mic acquisition completed (before joining). |
| [`camera-error`](/reference/daily-js/events/settings-events#camera-error) | `DailyEventObjectCameraError` | Camera or mic acquisition failed. |
| [`joining-meeting`](/reference/daily-js/events/lifecycle-events#joining-meeting) | `DailyEventObjectNoPayload` | `join()` was called; connection in progress. |
| [`joined-meeting`](/reference/daily-js/events/lifecycle-events#joined-meeting) | `DailyEventObjectParticipants` | Successfully joined. `participants` contains the full initial snapshot. |
| [`left-meeting`](/reference/daily-js/events/lifecycle-events#left-meeting) | `DailyEventObjectNoPayload` | `leave()` or ejection completed. |
| [`call-instance-destroyed`](/reference/daily-js/events/lifecycle-events#call-instance-destroyed) | `DailyEventObjectNoPayload` | `destroy()` was called. |
| [`access-state-updated`](/reference/daily-js/events/lifecycle-events#access-state-updated) | `DailyEventObjectAccessState` | Room access level changed (e.g. knock-to-join lobby admission). |

```typescript theme={null}
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`](/reference/daily-js/events/participant-events#participant-joined) | `DailyEventObjectParticipant` | A remote participant entered the room. |
| [`participant-updated`](/reference/daily-js/events/participant-events#participant-updated) | `DailyEventObjectParticipant` | Any field on a participant changed. |
| [`participant-left`](/reference/daily-js/events/participant-events#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`](/reference/daily-js/events/participant-events#participant-counts-updated) | `DailyEventObjectParticipantCounts` | Aggregate `present`/`hidden` counts changed. |
| [`active-speaker-change`](/reference/daily-js/events/participant-events#active-speaker-change) | `DailyEventObjectActiveSpeakerChange` | The dominant speaker changed. `activeSpeaker.peerId` is the new speaker's `session_id`. |
| [`waiting-participant-added`](/reference/daily-js/events/participant-events#waiting-participant-added) | `DailyEventObjectWaitingParticipant` | A participant entered the knock-to-join lobby. |
| [`waiting-participant-updated`](/reference/daily-js/events/participant-events#waiting-participant-updated) | `DailyEventObjectWaitingParticipant` | A waiting participant's state changed. |
| [`waiting-participant-removed`](/reference/daily-js/events/participant-events#waiting-participant-removed) | `DailyEventObjectWaitingParticipant` | A waiting participant was admitted, denied, or left. |

```typescript theme={null}
call.on('active-speaker-change', ({ activeSpeaker }) => {
  highlightTile(activeSpeaker.peerId);
});

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

### Media Events

| Event | Payload type | Description |
| - | - | - |
| [`track-started`](/reference/daily-js/events/media-events#track-started) | `DailyEventObjectTrack` | A track became `playable`. `type` is `'video'`, `'audio'`, `'screenVideo'`, `'screenAudio'`, or a custom name. |
| [`track-stopped`](/reference/daily-js/events/media-events#track-stopped) | `DailyEventObjectTrack` | A track left `playable` state. |
| [`local-screen-share-started`](/reference/daily-js/events/media-events#local-screen-share-started) | — | The local participant's screen share started. |
| [`local-screen-share-stopped`](/reference/daily-js/events/media-events#local-screen-share-stopped) | — | The local screen share stopped normally. |
| [`local-screen-share-canceled`](/reference/daily-js/events/media-events#local-screen-share-canceled) | — | The user dismissed the browser's screen picker. |
| [`local-audio-level`](/reference/daily-js/events/media-events#local-audio-level) | `DailyEventObjectLocalAudioLevel` | Local audio level update. `audioLevel` is `0.0–1.0`. Requires `startLocalAudioLevelObserver()`. |
| [`remote-participants-audio-level`](/reference/daily-js/events/media-events#remote-participants-audio-level) | `DailyEventObjectRemoteParticipantsAudioLevel` | Map of `session_id → audioLevel` for remote participants. Requires `startRemoteParticipantsAudioLevelObserver()`. |
| [`face-counts-updated`](/reference/daily-js/events/media-events#face-counts-updated) | `DailyEventObjectFaceCounts` | Number of faces detected in local video changed. |

```typescript theme={null}
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

<Badge color="blue">Paid plans only</Badge>

| Event | Payload type | Description |
| - | - | - |
| [`recording-started`](/reference/daily-js/events/recording-events#recording-started) | `DailyEventObjectRecordingStarted` | A recording started. `local` is `true` for browser-based recording. |
| [`recording-stopped`](/reference/daily-js/events/recording-events#recording-stopped) | `DailyEventObjectRecordingStopped` | A recording stopped. |
| [`recording-error`](/reference/daily-js/events/recording-events#recording-error) | `DailyEventObjectRecordingError` | Recording encountered an error. |

```typescript theme={null}
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

<Badge color="blue">Paid plans only</Badge>

| Event | Payload type | Description |
| - | - | - |
| [`live-streaming-started`](/reference/daily-js/events/live-streaming-events#live-streaming-started) | `DailyEventObjectLiveStreamingStarted` | Live stream started. |
| [`live-streaming-updated`](/reference/daily-js/events/live-streaming-events#live-streaming-updated) | `DailyEventObjectLiveStreamingUpdated` | Stream state or endpoint changed. `state` is `'connected'` or `'interrupted'`. |
| [`live-streaming-stopped`](/reference/daily-js/events/live-streaming-events#live-streaming-stopped) | `DailyEventObjectLiveStreamingStopped` | Live stream stopped. |
| [`live-streaming-error`](/reference/daily-js/events/live-streaming-events#live-streaming-error) | `DailyEventObjectLiveStreamingError` | Live streaming error. |

### Transcription Events

<Badge color="blue">Paid plans only</Badge>

| Event | Payload type | Description |
| - | - | - |
| [`transcription-started`](/reference/daily-js/events/transcription-events#transcription-started) | `DailyEventObjectTranscriptionStarted` | Transcription started. Contains model, language, and configuration details. |
| [`transcription-stopped`](/reference/daily-js/events/transcription-events#transcription-stopped) | `DailyEventObjectTranscriptionStopped` | Transcription stopped. |
| [`transcription-error`](/reference/daily-js/events/transcription-events#transcription-error) | `DailyEventObjectTranscriptionError` | Transcription error. |
| [`transcription-message`](/reference/daily-js/events/transcription-events#transcription-message) | `DailyEventObjectTranscriptionMessage` | A transcription result. `text` is the transcript; `participantId` identifies the speaker. |

```typescript theme={null}
call.on('transcription-message', ({ participantId, text, timestamp }) => {
  addCaption({ participantId, text, timestamp });
});
```

### Network Events

| Event | Payload type | Description |
| - | - | - |
| [`network-quality-change`](/reference/daily-js/events/network-events#network-quality-change) | `DailyEventObjectNetworkQualityEvent` | Network quality changed. `networkState` is `'good'`, `'warning'`, `'bad'`, or `'unknown'`. |
| [`network-connection`](/reference/daily-js/events/network-events#network-connection) | `DailyEventObjectNetworkConnectionEvent` | Low-level connection event. `type` is `'signaling'`, `'peer-to-peer'`, or `'sfu'`. |
| [`cpu-load-change`](/reference/daily-js/events/network-events#cpu-load-change) | `DailyEventObjectCpuLoadEvent` | CPU load state changed. `cpuLoadState` is `'low'` or `'high'`. |
| [`test-completed`](/reference/daily-js/events/network-events#test-completed) | `DailyEventObjectTestCompleted` | A quality or connectivity test finished. |

```typescript theme={null}
call.on('network-quality-change', ({ networkState, networkStateReasons }) => {
  if (networkState === 'bad') {
    showNetworkWarning(networkStateReasons);
  }
});
```

### Settings & Device Events

| Event | Payload type | Description |
| - | - | - |
| [`available-devices-updated`](/reference/daily-js/events/settings-events#available-devices-updated) | `DailyEventObjectAvailableDevicesUpdated` | The list of available input/output devices changed. |
| [`selected-devices-updated`](/reference/daily-js/events/settings-events#selected-devices-updated) | `DailyEventObjectSelectedDevicesUpdated` | The active camera, mic, or speaker selection changed. |
| [`input-settings-updated`](/reference/daily-js/events/settings-events#input-settings-updated) | `DailyEventObjectInputSettingsUpdated` | Input settings (noise cancellation, background blur, etc.) changed. |
| [`send-settings-updated`](/reference/daily-js/events/settings-events#send-settings-updated) | `DailyEventObjectSendSettingsUpdated` | Video send settings (bitrate, simulcast) changed. |
| [`receive-settings-updated`](/reference/daily-js/events/settings-events#receive-settings-updated) | `DailyEventObjectReceiveSettingsUpdated` | Receive settings (simulcast layer) changed (call object mode). |

### Messaging Events

| Event | Payload type | Description |
| - | - | - |
| [`app-message`](/reference/daily-js/events/messaging-events#app-message) | `DailyEventObjectAppMessage` | A message sent via `sendAppMessage()`. `data` is the payload; `fromId` is the sender's `session_id`. |
| [`meeting-session-updated`](/reference/daily-js/events/messaging-events#meeting-session-updated) | (deprecated) | Use `meeting-session-summary-updated` instead. |
| [`meeting-session-summary-updated`](/reference/daily-js/events/messaging-events#meeting-session-summary-updated) | `DailyEventObjectMeetingSessionSummaryUpdated` | The meeting session summary changed. |
| [`meeting-session-state-updated`](/reference/daily-js/events/messaging-events#meeting-session-state-updated) | `DailyEventObjectMeetingSessionStateUpdated` | Shared meeting session state changed. |

```typescript theme={null}
// 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)

<Badge color="blue">Paid plans only</Badge>

| Event | Payload type | Description |
| - | - | - |
| [`dialin-ready`](/reference/daily-js/events/telephony-events#dialin-ready) | `DailyEventObjectDialinReady` | SIP endpoint is ready to accept dial-in calls. `sipEndpoint` is the address. |
| [`dialin-connected`](/reference/daily-js/events/telephony-events#dialin-connected) | `DailyEventObjectDialinConnected` | A dial-in call connected. |
| [`dialin-error`](/reference/daily-js/events/telephony-events#dialin-error) | `DailyEventObjectDialinError` | Dial-in error. |
| [`dialin-stopped`](/reference/daily-js/events/telephony-events#dialin-stopped) | `DailyEventObjectDialinStopped` | Dial-in call stopped. |
| [`dialin-warning`](/reference/daily-js/events/telephony-events#dialin-warning) | `DailyEventObjectDialinWarning` | Non-fatal dial-in warning. |
| [`dialout-connected`](/reference/daily-js/events/telephony-events#dialout-connected) | `DailyEventObjectDialOutConnected` | An outbound dial-out call connected. |
| [`dialout-answered`](/reference/daily-js/events/telephony-events#dialout-answered) | `DailyEventObjectDialOutAnswered` | An outbound call was answered. |
| [`dialout-error`](/reference/daily-js/events/telephony-events#dialout-error) | `DailyEventObjectDialOutError` | Dial-out error. |
| [`dialout-stopped`](/reference/daily-js/events/telephony-events#dialout-stopped) | `DailyEventObjectDialOutStopped` | Dial-out call stopped. |
| [`dialout-warning`](/reference/daily-js/events/telephony-events#dialout-warning) | `DailyEventObjectDialOutWarning` | Non-fatal dial-out warning. |

### Error Events

| Event | Payload type | Description |
| - | - | - |
| [`error`](/reference/daily-js/events/error-events#error) | `DailyEventObjectFatalError` | A fatal error occurred. `error.type` is one of `DailyFatalErrorType`. The call ends automatically. |
| [`nonfatal-error`](/reference/daily-js/events/error-events#nonfatal-error) | `DailyEventObjectNonFatalError` | A recoverable error. `type` is one of `DailyNonFatalErrorType`. The call continues. |

```typescript theme={null}
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`](/reference/daily-js/events/iframe-ui-events#fullscreen) | The iframe entered fullscreen. |
| [`exited-fullscreen`](/reference/daily-js/events/iframe-ui-events#exited-fullscreen) | The iframe exited fullscreen. |
| [`pip-started`](/reference/daily-js/events/iframe-ui-events#pip-started) | Picture-in-picture started. |
| [`pip-stopped`](/reference/daily-js/events/iframe-ui-events#pip-stopped) | Picture-in-picture stopped. |
| [`sidebar-view-changed`](/reference/daily-js/events/iframe-ui-events#sidebar-view-changed) | The active sidebar panel changed. |
| [`custom-button-click`](/reference/daily-js/events/iframe-ui-events#custom-button-click) | A custom tray button was clicked. |
| [`active-speaker-mode-change`](/reference/daily-js/events/iframe-ui-events#active-speaker-mode-change) | Active speaker mode was toggled. |
| [`show-local-video-changed`](/reference/daily-js/events/iframe-ui-events#show-local-video-changed) | Whether the local video tile is visible changed. |
| [`lang-updated`](/reference/daily-js/events/iframe-ui-events#lang-updated) | The UI language changed. |
| [`theme-updated`](/reference/daily-js/events/iframe-ui-events#theme-updated) | The UI theme changed. |

## Removing Listeners

Use `call.off()` to remove a specific listener:

```typescript theme={null}
// 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:

```typescript theme={null}
// 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();
```

<Warning>
  Always remove event listeners before calling `call.destroy()` to prevent memory leaks, especially in single-page applications that re-render frequently.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.