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

# Events & Callbacks

> How to respond to bot and session events in Pipecat client applications.

<View title="React" icon="react">
  <Callout icon="react" color="#FFC107">
    You are currently viewing the React version of this page. Use the dropdown to the right to customize this page for your client framework.
  </Callout>
</View>

<View title="JavaScript" icon="js">
  <Callout icon="js" color="#FFC107">
    You are currently viewing the JavaScript version of this page. Use the dropdown to the right to customize this page for your client framework.
  </Callout>
</View>

<View title="React Native" icon="mobile">
  <Callout icon="mobile" color="#FFC107">
    You are currently viewing the React Native version of this page. Use the dropdown to the right to customize this page for your client framework.
  </Callout>
</View>

<View title="iOS" icon="apple">
  <Callout icon="apple" color="#FFC107">
    You are currently viewing the iOS version of this page. Use the dropdown to the right to customize this page for your client framework.
  </Callout>
</View>

The Pipecat client emits events throughout the session lifecycle — when the bot connects, when the user speaks, when a transcript arrives, and more.

## Subscribing to events

### Callbacks

<View title="React" icon="react">
  Pass handlers in the `PipecatClient` constructor. Good for events you always want to handle, defined once at setup:

  ```tsx theme={null}
  const client = new PipecatClient({
    transport: new DailyTransport(),
    callbacks: {
      onBotReady: () => console.log("Bot is ready"),
      onUserTranscript: (data) => console.log("User said:", data.text),
    },
  });
  ```
</View>

<View title="JavaScript" icon="js">
  Pass handlers in the `PipecatClient` constructor. Good for events you always want to handle, defined once at setup:

  ```tsx theme={null}
  const client = new PipecatClient({
    transport: new DailyTransport(),
    callbacks: {
      onBotReady: () => console.log("Bot is ready"),
      onUserTranscript: (data) => console.log("User said:", data.text),
    },
  });
  ```
</View>

<View title="React Native" icon="mobile">
  Pass handlers in the `PipecatClient` constructor. Good for events you always want to handle, defined once at setup:

  ```tsx theme={null}
  const client = new PipecatClient({
    transport: new DailyTransport(),
    callbacks: {
      onBotReady: () => console.log("Bot is ready"),
      onUserTranscript: (data) => console.log("User said:", data.text),
    },
  });
  ```
</View>

<View title="iOS" icon="apple">
  iOS uses the delegate pattern rather than constructor callbacks — see [Event listeners](#event-listeners) below.
</View>

### Event listeners

<View title="React" icon="react">
  In React, use the `useRTVIClientEvent` hook to subscribe within a component. It handles registration and cleanup automatically when the component mounts and unmounts:

  ```tsx theme={null}
  import { RTVIEvent } from "@pipecat-ai/client-js";
  import { useRTVIClientEvent } from "@pipecat-ai/client-react";

  function TranscriptDisplay() {
    useRTVIClientEvent(
      RTVIEvent.UserTranscript,
      useCallback((data) => {
        if (data.final) setTranscript(data.text);
      }, [])
    );
  }
  ```

  You can also use `.on()` directly on the client instance, but `useRTVIClientEvent` is preferred in React since it avoids stale closure issues and cleans up automatically.
</View>

<View title="JavaScript" icon="js">
  Add handlers with `.on()` at any point — useful for dynamic subscriptions or when you want to add and remove handlers at runtime:

  ```tsx theme={null}
  client.on(RTVIEvent.BotReady, () => console.log("Bot is ready"));
  client.on(RTVIEvent.UserTranscript, (data) => console.log("User said:", data.text));
  ```

  Callbacks and event listeners are equivalent — use whichever pattern fits your architecture.
</View>

<View title="React Native" icon="mobile">
  Use `useEffect` with `.on()` and `.off()` to subscribe and clean up within a component:

  ```tsx theme={null}
  import { RTVIEvent, TranscriptData } from "@pipecat-ai/client-js";
  import { useEffect } from "react";

  function TranscriptDisplay() {
    useEffect(() => {
      const handler = (data: TranscriptData) => {
        if (data.final) setTranscript(data.text);
      };
      client.on(RTVIEvent.UserTranscript, handler);
      return () => client.off(RTVIEvent.UserTranscript, handler);
    }, []);
  }
  ```

  Always return a cleanup function from `useEffect` to remove the listener when the component unmounts.
</View>

<View title="iOS" icon="apple">
  Conform your model to `PipecatClientDelegate` and assign it as the delegate after creating the client. `PipecatClient` is `@MainActor`, so create and use it from a `@MainActor` context. All delegate methods are optional — implement only what you need:

  ```swift theme={null}
  // Inside a @MainActor function (e.g. your connect() method)
  let options = PipecatClientOptions(transport: SmallWebRTCTransport(), enableMic: true)
  let client = PipecatClient(options: options)
  client.delegate = self
  ```

  ```swift theme={null}
  extension MyModel: PipecatClientDelegate {
    func onBotReady(botReadyData: BotReadyData) {
      Task { @MainActor in
        print("Bot is ready")
      }
    }

    func onUserTranscript(data: Transcript) {
      Task { @MainActor in
        if data.final ?? false { setTranscript(data.text) }
      }
    }
  }
  ```

  Delegate callbacks may arrive off the main actor — always use `Task { @MainActor in }` before updating `@Published` properties or any UI state.
</View>

***

## Event reference

### Session and connectivity

These events track the connection state of the client and bot. See [Session Lifecycle](/client/concepts/session-lifecycle) for the full state progression.

| Event                   | Callback                  | When it fires                                                                               |
| ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------- |
| `Connected`             | `onConnected`             | Client transport connection established                                                     |
| `Disconnected`          | `onDisconnected`          | Client disconnected (intentional or error)                                                  |
| `TransportStateChanged` | `onTransportStateChanged` | Any transport state change; receives the new `TransportState` string                        |
| `BotConnected`          | `onBotConnected`          | Bot joined the transport; pipeline may still be initializing                                |
| `BotReady`              | `onBotReady`              | Bot pipeline is ready; safe to send messages and expect audio                               |
| `BotDisconnected`       | `onBotDisconnected`       | Bot left the session; client will also disconnect unless `disconnectOnBotDisconnect: false` |
| `ParticipantConnected`  | `onParticipantJoined`     | Any participant joined (bot, local, or other)                                               |
| `ParticipantLeft`       | `onParticipantLeft`       | Any participant left (bot, local, or other)                                                 |

<Tip>
  `BotReady` receives a `BotReadyData` object with a `version` field — the RTVI version the bot is running. You can use this to check compatibility if your client and server may be on different versions.
</Tip>

### Voice activity

These events are driven by the bot's VAD (voice activity detection) model. VAD is smarter than tracking raw audio levels — it understands turn-taking, so it can distinguish between a user who has finished speaking and one who has simply paused or is speaking slowly.

| Event                 | Callback                | When it fires                                           |
| --------------------- | ----------------------- | ------------------------------------------------------- |
| `UserStartedSpeaking` | `onUserStartedSpeaking` | VAD detected the user started speaking                  |
| `UserStoppedSpeaking` | `onUserStoppedSpeaking` | VAD detected the user stopped speaking                  |
| `BotStartedSpeaking`  | `onBotStartedSpeaking`  | Bot started sending audio                               |
| `BotStoppedSpeaking`  | `onBotStoppedSpeaking`  | Bot stopped sending audio                               |
| `LocalAudioLevel`     | `onLocalAudioLevel`     | Local audio gain level (0–1); fires continuously        |
| `RemoteAudioLevel`    | `onRemoteAudioLevel`    | Remote audio gain level (0–1); fires continuously       |
| `UserMuteStarted`     | `onUserMuteStarted`     | Server started ignoring client audio (server-side mute) |
| `UserMuteStopped`     | `onUserMuteStopped`     | Server resumed processing client audio                  |

<Tip>
  `UserMuteStarted`/`UserMuteStopped` reflect server-side muting — the client continues sending audio, but the bot is ignoring it. Use these to update your UI (e.g., show a muted indicator) without actually stopping the local mic.
</Tip>

### Transcription and bot output

| Event            | Callback           | Data             | When it fires                                                                      |
| ---------------- | ------------------ | ---------------- | ---------------------------------------------------------------------------------- |
| `UserTranscript` | `onUserTranscript` | `TranscriptData` | User speech transcribed; fires for both partial (`final: false`) and final results |
| `BotOutput`      | `onBotOutput`      | `BotOutputData`  | Bot text output, typically aggregated by sentence or word during TTS synthesis     |
| `BotLlmText`     | `onBotLlmText`     | `BotLLMTextData` | Raw LLM token stream                                                               |
| `BotLlmStarted`  | `onBotLlmStarted`  | —                | LLM inference started                                                              |
| `BotLlmStopped`  | `onBotLlmStopped`  | —                | LLM inference finished                                                             |
| `BotTtsText`     | `onBotTtsText`     | `BotTTSTextData` | Words from TTS as they are synthesized (streaming TTS only)                        |
| `BotTtsStarted`  | `onBotTtsStarted`  | —                | TTS synthesis started                                                              |
| `BotTtsStopped`  | `onBotTtsStopped`  | —                | TTS synthesis finished                                                             |

`UserTranscript` fires continuously as speech is recognized. Check `data.final` to distinguish committed transcripts from work-in-progress partials:

<View title="React" icon="react">
  ```tsx theme={null}
  useRTVIClientEvent(
    RTVIEvent.UserTranscript,
    useCallback((data) => {
      if (data.final) {
        addMessage(data.text); // committed
      } else {
        updatePartial(data.text); // still in progress
      }
    }, [])
  );
  ```
</View>

<View title="JavaScript" icon="js">
  ```tsx theme={null}
  client.on(RTVIEvent.UserTranscript, (data) => {
    if (data.final) {
      addMessage(data.text); // committed
    } else {
      updatePartial(data.text); // still in progress
    }
  });
  ```
</View>

<View title="React Native" icon="mobile">
  ```tsx theme={null}
  useEffect(() => {
    const handler = (data: TranscriptData) => {
      if (data.final) {
        addMessage(data.text); // committed
      } else {
        updatePartial(data.text); // still in progress
      }
    };
    client.on(RTVIEvent.UserTranscript, handler);
    return () => client.off(RTVIEvent.UserTranscript, handler);
  }, []);
  ```
</View>

<View title="iOS" icon="apple">
  ```swift theme={null}
  func onUserTranscript(data: Transcript) {
    Task { @MainActor in
      if data.final ?? false {
        addMessage(data.text) // committed
      } else {
        updatePartial(data.text) // still in progress
      }
    }
  }
  ```
</View>

`BotOutput` is the recommended way to display the bot's response text. It provides the best possible representation of what the bot is saying — supporting interruptions and unspoken responses. By default, Pipecat aggregates output by sentences and words (assuming your TTS supports streaming), but custom aggregation strategies are supported too - like breaking out code snippets or other structured content:

<View title="React" icon="react">
  ```tsx theme={null}
  useRTVIClientEvent(
    RTVIEvent.BotOutput,
    useCallback((data) => {
      if (data.aggregated_by === "sentence") {
        appendSentence(data.text);
      }
    }, [])
  );
  ```
</View>

<View title="JavaScript" icon="js">
  ```tsx theme={null}
  client.on(RTVIEvent.BotOutput, (data) => {
    if (data.aggregated_by === "sentence") {
      appendSentence(data.text);
    }
  });
  ```
</View>

<View title="React Native" icon="mobile">
  ```tsx theme={null}
  useEffect(() => {
    const handler = (data: BotOutputData) => {
      if (data.aggregated_by === "sentence") {
        appendSentence(data.text);
      }
    };
    client.on(RTVIEvent.BotOutput, handler);
    return () => client.off(RTVIEvent.BotOutput, handler);
  }, []);
  ```
</View>

<View title="iOS" icon="apple">
  The iOS SDK exposes `onBotOutput` for the bot's text output:

  ```swift theme={null}
  func onBotOutput(data: BotOutputData) {
    Task { @MainActor in
      appendSentence(data.text)
    }
  }
  ```
</View>

### Errors

| Event          | Callback         | When it fires                                                                  |
| -------------- | ---------------- | ------------------------------------------------------------------------------ |
| `Error`        | `onError`        | Bot signalled an error; `data.fatal` is `true` if the session is unrecoverable |
| `MessageError` | `onMessageError` | A client message failed or got an error response                               |

Always handle `Error`. If `data.fatal` is `true`, the bot has already disconnected — update your UI accordingly:

<View title="React" icon="react">
  ```tsx theme={null}
  useRTVIClientEvent(
    RTVIEvent.Error,
    useCallback(({ data }) => {
      if (data.fatal) {
        showReconnectPrompt(data.message);
      } else {
        showToast(data.message);
      }
    }, [])
  );
  ```
</View>

<View title="JavaScript" icon="js">
  ```tsx theme={null}
  client.on(RTVIEvent.Error, ({ data }) => {
    if (data.fatal) {
      showReconnectPrompt(data.message);
    } else {
      showToast(data.message);
    }
  });
  ```
</View>

<View title="React Native" icon="mobile">
  ```tsx theme={null}
  useEffect(() => {
    const handler = ({ data: ErrorData }) => {
      if (data.fatal) {
        showReconnectPrompt(data.message);
      } else {
        showToast(data.message);
      }
    };
    client.on(RTVIEvent.Error, handler);
    return () => client.off(RTVIEvent.Error, handler);
  }, []);
  ```
</View>

<View title="iOS" icon="apple">
  ```swift theme={null}
  func onError(message: RTVIMessageInbound) {
    Task { @MainActor in
      // message.data contains the error description string
      showError(message.data ?? "Unknown error")
    }
  }
  ```
</View>

### Devices and tracks

| Event                      | Callback                     | When it fires                                  |
| -------------------------- | ---------------------------- | ---------------------------------------------- |
| `AvailableMicsUpdated`     | `onAvailableMicsUpdated`     | Mic list changed or `initDevices()` called     |
| `AvailableCamsUpdated`     | `onAvailableCamsUpdated`     | Camera list changed or `initDevices()` called  |
| `AvailableSpeakersUpdated` | `onAvailableSpeakersUpdated` | Speaker list changed or `initDevices()` called |
| `MicUpdated`               | `onMicUpdated`               | Active microphone changed                      |
| `CamUpdated`               | `onCamUpdated`               | Active camera changed                          |
| `SpeakerUpdated`           | `onSpeakerUpdated`           | Active speaker changed                         |
| `DeviceError`              | `onDeviceError`              | Mic, camera, or permission error               |
| `TrackStarted`             | `onTrackStarted`             | A media track (audio or video) became playable |
| `TrackStopped`             | `onTrackStopped`             | A media track stopped                          |

### Function calling

<View title="React" icon="react">
  These events fire when the bot's LLM makes a function call. Use them to track status and display relevant UI (e.g., a loading spinner while the call is in progress).

  | Event                       | Callback                      | When it fires                                                          |
  | --------------------------- | ----------------------------- | ---------------------------------------------------------------------- |
  | `LLMFunctionCallStarted`    | `onLLMFunctionCallStarted`    | LLM initiated a function call                                          |
  | `LLMFunctionCallInProgress` | `onLLMFunctionCallInProgress` | Function call is executing; triggers registered `FunctionCallHandler`s |
  | `LLMFunctionCallStopped`    | `onLLMFunctionCallStopped`    | Function call completed or was cancelled                               |
</View>

<View title="JavaScript" icon="js">
  These events fire when the bot's LLM makes a function call. Use them to track status and display relevant UI (e.g., a loading spinner while the call is in progress).

  | Event                       | Callback                      | When it fires                                                          |
  | --------------------------- | ----------------------------- | ---------------------------------------------------------------------- |
  | `LLMFunctionCallStarted`    | `onLLMFunctionCallStarted`    | LLM initiated a function call                                          |
  | `LLMFunctionCallInProgress` | `onLLMFunctionCallInProgress` | Function call is executing; triggers registered `FunctionCallHandler`s |
  | `LLMFunctionCallStopped`    | `onLLMFunctionCallStopped`    | Function call completed or was cancelled                               |
</View>

<View title="React Native" icon="mobile">
  These events fire when the bot's LLM makes a function call. Use them to track status and display relevant UI (e.g., a loading spinner while the call is in progress).

  | Event                       | Callback                      | When it fires                                                          |
  | --------------------------- | ----------------------------- | ---------------------------------------------------------------------- |
  | `LLMFunctionCallStarted`    | `onLLMFunctionCallStarted`    | LLM initiated a function call                                          |
  | `LLMFunctionCallInProgress` | `onLLMFunctionCallInProgress` | Function call is executing; triggers registered `FunctionCallHandler`s |
  | `LLMFunctionCallStopped`    | `onLLMFunctionCallStopped`    | Function call completed or was cancelled                               |
</View>

<View title="iOS" icon="apple">
  iOS uses a single `onLLMFunctionCall` delegate method for function calls. The SDK calls it with the function name, arguments, and an `onResult` closure you must invoke with the function's return value:

  ```swift theme={null}
  func onLLMFunctionCall(
    functionCallData: LLMFunctionCallData,
    onResult: ((Value) async -> Void)
  ) async {
    if functionCallData.functionName == "get_weather" {
      let city = functionCallData.args.asObject["city"]?.asString ?? ""
      let weather = await fetchWeather(city: city)
      await onResult(.string(weather))
    }
  }
  ```

  For multiple functions, use `registerFunctionCallHandler` to register per-function handlers instead of a single dispatch block:

  ```swift theme={null}
  client.registerFunctionCallHandler(functionName: "get_weather") { data, onResult in
    let city = data.args.asObject["city"]?.asString ?? ""
    let weather = await fetchWeather(city: city)
    await onResult(.string(weather))
  }
  ```
</View>

### Other

| Event           | Callback          | When it fires                                  |
| --------------- | ----------------- | ---------------------------------------------- |
| `ServerMessage` | `onServerMessage` | Custom message sent from the bot to the client |
| `Metrics`       | `onMetrics`       | Pipeline performance metrics from Pipecat      |

For custom server\<->client messaging, see [Custom Messaging](/client/guides/custom-messaging).

***

## API reference

<View title="React" icon="react">
  <CardGroup cols={2}>
    <Card title="React Hooks" icon="react" href="/api-reference/client/react/hooks">
      `useRTVIClientEvent` and other React-specific event utilities
    </Card>

    <Card title="JavaScript SDK Callbacks" icon="js" href="/api-reference/client/js/callbacks">
      Complete callback signatures, data types, and transport compatibility
    </Card>
  </CardGroup>
</View>

<View title="JavaScript" icon="js">
  <CardGroup cols={1}>
    <Card title="JavaScript SDK Callbacks" icon="js" href="/api-reference/client/js/callbacks">
      Complete callback signatures, data types, and transport compatibility
    </Card>
  </CardGroup>
</View>

<View title="React Native" icon="mobile">
  <CardGroup cols={1}>
    <Card title="JavaScript SDK Callbacks" icon="js" href="/api-reference/client/js/callbacks">
      Complete callback signatures, data types, and transport compatibility
    </Card>
  </CardGroup>
</View>

<View title="iOS" icon="apple">
  <CardGroup cols={1}>
    <Card title="iOS SDK Reference" icon="apple" href="/api-reference/client/ios/overview">
      Full `PipecatClientDelegate` protocol and API reference
    </Card>
  </CardGroup>
</View>
