> ## 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.

# Building a Voice UI

> Build a voice application from scratch using the Pipecat client SDK.

<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>

<View title="React" icon="react">
  This guide walks through building a React voice app without any UI abstractions — just the Pipecat React SDK directly. You'll see exactly how the client, provider, hooks, and audio output fit together, which is useful whether you're building a fully custom UI or want to understand what the [CLI-generated app](/client/get-started/quickstart) is doing under the hood.
</View>

<View title="JavaScript" icon="js">
  This guide walks through building a voice app in vanilla TypeScript — just the Pipecat JavaScript SDK directly, with no framework. You'll see how to set up the client, handle events, and wire up audio output manually.
</View>

<View title="React Native" icon="mobile">
  This guide walks through building a React Native voice app using the Pipecat JavaScript SDK. You'll see how to set up the client, subscribe to events with `useEffect`, and connect to your bot from a mobile app — with audio handled automatically by the platform.
</View>

<View title="iOS" icon="apple">
  This guide walks through building a SwiftUI voice app using the Pipecat iOS SDK. You'll see how to create the client, implement the delegate, and connect to your bot — with audio handled automatically by the SDK.
</View>

## Prerequisites

<View title="React" icon="react">
  * Node.js 18+
  * A running Pipecat bot
</View>

<View title="JavaScript" icon="js">
  * Node.js 18+
  * A running Pipecat bot
</View>

<View title="React Native" icon="mobile">
  * Node.js 18+
  * Xcode 15+ (for iOS) or Android Studio (for Android)
  * A running Pipecat bot
</View>

<View title="iOS" icon="apple">
  * Xcode 15+
  * A running Pipecat bot
</View>

<Card title="Don't have a bot yet?" icon="server" href="/pipecat/get-started/quickstart">
  Follow the Pipecat server quickstart to get a bot running locally at `http://localhost:7860`.
</Card>

## Installation

<View title="React" icon="react">
  Create a new React project and install the SDK, React bindings, and SmallWebRTC transport:

  ```bash theme={null}
  npm create vite@latest my-voice-app -- --template react-ts
  cd my-voice-app
  npm install @pipecat-ai/client-js @pipecat-ai/client-react @pipecat-ai/small-webrtc-transport
  ```
</View>

<View title="JavaScript" icon="js">
  Create a new TypeScript project and install the SDK and SmallWebRTC transport:

  ```bash theme={null}
  npm create vite@latest my-voice-app -- --template vanilla-ts
  cd my-voice-app
  npm install @pipecat-ai/client-js @pipecat-ai/small-webrtc-transport
  ```
</View>

<View title="React Native" icon="mobile">
  Create a new Expo project and install the SDK, SmallWebRTC transport, and media manager (For the most up-to-date installation instructions, see the [React Native Transport repo's README](https://github.com/pipecat-ai/pipecat-client-react-native-transports/tree/main/libs/daily-media-manager#installation)):

  ```bash theme={null}
  npx create-expo-app MyVoiceApp
  cd MyVoiceApp
  npx expo install @pipecat-ai/client-js @pipecat-ai/react-native-daily-media-manager @daily-co/react-native-daily-js@^0.82.0 @daily-co/react-native-webrtc@^124.0.6-daily.1 @react-native-async-storage/async-storage@^1.24.0 react-native-background-timer@^2.4.1 react-native-get-random-values@^1.11.0 @daily-co/config-plugin-rn-daily-js@0.0.11 expo-dev-client
  ```

  <Note>
    These packages use native modules, so they won't run in Expo Go. You'll need a
    [development build](https://docs.expo.dev/develop/development-builds/introduction/).
  </Note>

  Because this is a voice app, your `app.json` needs microphone permissions, the Daily config plugin, and minimum platform SDK versions:

  ```json theme={null}
  {
    "expo": {
      "ios": {
        "infoPlist": {
          "NSMicrophoneUsageDescription": "This app uses the microphone to talk to your voice AI assistant.",
          "UIBackgroundModes": ["voip"]
        },
        "bundleIdentifier": "co.daily.expo.SmallWebRTCDemo",
      },
      "android": {
        "package": "co.daily.expo.SmallWebRTCDemo"
      },
      "plugins": [
        [
          "@daily-co/config-plugin-rn-daily-js",
          {
            "enableCamera": true,
            "enableMicrophone": true,
            "enableScreenShare": true
          }
        ],
        [
          "expo-build-properties",
          {
            "android": {
              "minSdkVersion": 24
            },
            "ios": {
              "deploymentTarget": "13.0"
            }
          }
        ]
      ]
    }
  }
  ```
</View>

<View title="iOS" icon="apple">
  In Xcode, add the following Swift packages via **File → Add Package Dependencies**:

  | Package               | URL                                                                 |
  | --------------------- | ------------------------------------------------------------------- |
  | Core SDK              | `https://github.com/pipecat-ai/pipecat-client-ios.git`              |
  | SmallWebRTC transport | `https://github.com/pipecat-ai/pipecat-client-ios-small-webrtc.git` |

  Then add `NSMicrophoneUsageDescription` to your `Info.plist`:

  ```xml theme={null}
  <key>NSMicrophoneUsageDescription</key>
  <string>This app uses the microphone to talk to your voice AI assistant.</string>
  ```
</View>

<Note>
  SmallWebRTC is ideal for development — no third-party account needed.
  For production, swap in the [Daily transport](/api-reference/client/js/transports/daily),
  which provides global infrastructure, echo cancellation, and more.
</Note>

## Step 1: Create the client

<View title="React" icon="react">
  Create the `PipecatClient` once at the module level with SmallWebRTC as the transport. Pass it to `PipecatClientProvider` so every component in your app can access it. `PipecatClientAudio` mounts a hidden `<audio>` element for the bot's voice output.

  Replace `src/main.tsx` with:

  ```tsx theme={null}
  import React from "react";
  import ReactDOM from "react-dom/client";
  import { PipecatClient } from "@pipecat-ai/client-js";
  import { SmallWebRTCTransport } from "@pipecat-ai/small-webrtc-transport";
  import { PipecatClientAudio, PipecatClientProvider } from "@pipecat-ai/client-react";
  import App from "./App";

  const client = new PipecatClient({
    transport: new SmallWebRTCTransport(),
    enableMic: true,
  });

  ReactDOM.createRoot(document.getElementById("root")!).render(
    <PipecatClientProvider client={client}>
      <App />
      <PipecatClientAudio />
    </PipecatClientProvider>
  );
  ```

  Two things to note here:

  * **`PipecatClientProvider`** makes the client available to all child components via hooks.
  * **`PipecatClientAudio`** is a headless component that mounts the bot's audio output. It needs to live inside the provider, but has no visible UI.
</View>

<View title="JavaScript" icon="js">
  Create the `PipecatClient` and wire up the bot's audio output manually. Replace `src/main.ts` with:

  ```ts theme={null}
  import { PipecatClient, RTVIEvent } from "@pipecat-ai/client-js";
  import { SmallWebRTCTransport } from "@pipecat-ai/small-webrtc-transport";

  export const client = new PipecatClient({
    transport: new SmallWebRTCTransport(),
    enableMic: true,
  });

  // Mount a hidden <audio> element for bot voice output
  const audioEl = document.createElement("audio");
  audioEl.autoplay = true;
  document.body.appendChild(audioEl);

  client.on(RTVIEvent.TrackStarted, (track, participant) => {
    if (!participant?.local && track.kind === "audio") {
      audioEl.srcObject = new MediaStream([track]);
    }
  });
  ```
</View>

<View title="React Native" icon="mobile">
  Create the `PipecatClient` with the React Native SmallWebRTC transport and `DailyMediaManager`. Create `lib/client.ts` at the project root:

  ```ts theme={null}
  import { PipecatClient } from "@pipecat-ai/client-js";
  import { RNSmallWebRTCTransport } from "@pipecat-ai/react-native-small-webrtc-transport";
  import { DailyMediaManager } from "@pipecat-ai/react-native-daily-media-manager";

  export const client = new PipecatClient({
    transport: new RNSmallWebRTCTransport({ mediaManager: new DailyMediaManager() }),
    enableMic: true,
  });
  ```

  Audio output is handled automatically — no `<audio>` element or `PipecatClientAudio` needed.
</View>

<View title="iOS" icon="apple">
  Create a model class that holds the `PipecatClient` and conforms to `PipecatClientDelegate`. Create `VoiceClientModel.swift`:

  ```swift theme={null}
  import Foundation
  import PipecatClientIOS
  import PipecatClientIOSSmallWebrtc

  class VoiceClientModel: ObservableObject {
    @Published var transportState: TransportState = .disconnected
    private var client: PipecatClient?

    init() {
      PipecatClientIOS.setLogLevel(.warn)
    }
  }
  extension CallContainerModel:PipecatClientDelegate {
  }
  ```

  Audio output is handled automatically — no additional setup required.
</View>

## Step 2: Build the app

<View title="React" icon="react">
  Replace `src/App.tsx` with:

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

  export default function App() {
    const client = usePipecatClient();
    const transportState = usePipecatClientTransportState();
    const { messages } = usePipecatConversation();

    const isConnected = transportState === "ready";
    const isConnecting = ["authenticating", "connecting", "connected"].includes(transportState);

    useRTVIClientEvent(
      RTVIEvent.Error,
      useCallback((error) => console.error("Bot error:", error), [])
    );

    const handleConnect = async () => {
      await client.connect({ webrtcRequestParams: { endpoint: "http://localhost:7860/api/offer" } });
    };

    return (
      <div style={{ maxWidth: 600, margin: "40px auto", fontFamily: "sans-serif" }}>
        <h1>Pipecat Voice App</h1>

        <div style={{ marginBottom: 16 }}>
          <button
            onClick={isConnected ? () => client.disconnect() : handleConnect}
            disabled={isConnecting}
          >
            {isConnected ? "Disconnect" : isConnecting ? "Connecting…" : "Connect"}
          </button>
          <span style={{ marginLeft: 12, color: "#666" }}>
            {transportState}
          </span>
        </div>

        <ul style={{ listStyle: "none", padding: 0 }}>
          {messages.map((msg, i) => (
            <li key={i} style={{ marginBottom: 8 }}>
              <strong>{msg.role === "user" ? "You" : "Bot"}:</strong>{" "}
              {msg.parts?.map((part, j) => (
                <span key={j}>
                  {typeof part.text === "string"
                    ? part.text
                    : part.text.spoken + part.text.unspoken}
                </span>
              ))}
            </li>
          ))}
        </ul>
      </div>
    );
  }
  ```
</View>

<View title="JavaScript" icon="js">
  Replace `src/app.ts` with:

  ```ts theme={null}
  import { RTVIEvent } from "@pipecat-ai/client-js";
  import { client } from "./main";

  const button = document.getElementById("connect-btn") as HTMLButtonElement;
  const statusEl = document.getElementById("status") as HTMLSpanElement;
  const messageList = document.getElementById("messages") as HTMLUListElement;

  client.on(RTVIEvent.TransportStateChanged, (state) => {
    const isConnected = state === "ready";
    const isConnecting = ["authenticating", "connecting", "connected"].includes(state);

    statusEl.textContent = state;
    button.textContent = isConnected ? "Disconnect" : isConnecting ? "Connecting…" : "Connect";
    button.disabled = isConnecting;
  });

  client.on(RTVIEvent.BotOutput, (data) => {
    if (data.aggregated_by === "sentence") {
      appendMessage("Bot", data.text);
    }
  });

  client.on(RTVIEvent.UserTranscript, (data) => {
    if (data.final) appendMessage("You", data.text);
  });

  client.on(RTVIEvent.Error, (error) => console.error("Bot error:", error));

  button.addEventListener("click", async () => {
    if (client.state === "ready") {
      await client.disconnect();
    } else {
      await client.connect({ webrtcRequestParams: { endpoint: "http://localhost:7860/api/offer" } });
    }
  });

  function appendMessage(role: string, text: string) {
    const li = document.createElement("li");
    li.style.marginBottom = "8px";
    li.innerHTML = `<strong>${role}:</strong> ${text}`;
    messageList.appendChild(li);
  }
  ```

  And update `index.html` to include the elements the script expects:

  ```html theme={null}
  <div style="max-width: 600px; margin: 40px auto; font-family: sans-serif;">
    <h1>Pipecat Voice App</h1>
    <div style="margin-bottom: 16px;">
      <button id="connect-btn">Connect</button>
      <span id="status" style="margin-left: 12px; color: #666;"></span>
    </div>
    <ul id="messages" style="list-style: none; padding: 0;"></ul>
  </div>
  <script type="module" src="/src/app.ts"></script>
  ```
</View>

<View title="React Native" icon="mobile">
  Replace `app/(tabs)/index.tsx` with:

  ```tsx theme={null}
  import React, { useState, useEffect } from "react";
  import { View, Text, Button, ScrollView, StyleSheet, Platform } from "react-native";
  import { RTVIEvent } from "@pipecat-ai/client-js";
  import { client } from "@/lib/client";

  // Android emulators can't reach the host via `localhost` — use 10.0.2.2 instead.
  // On a physical device, replace this with your machine's local network IP.
  const BOT_URL =
    Platform.OS === "android"
      ? "http://10.0.2.2:7860/api/offer"
      : "http://localhost:7860/api/offer";

  interface Message {
    role: "user" | "bot";
    text: string;
  }

  export default function App() {
    const [transportState, setTransportState] = useState(client.state);
    const [messages, setMessages] = useState<Message[]>([]);

    useEffect(() => {
      const onStateChange = (state: string) => setTransportState(state);
      const onBotOutput = (data: any) => {
        if (data.aggregated_by === "sentence") {
          setMessages((prev) => [...prev, { role: "bot", text: data.text }]);
        }
      };
      const onUserTranscript = (data: any) => {
        if (data.final) {
          setMessages((prev) => [...prev, { role: "user", text: data.text }]);
        }
      };
      const onError = (error: any) => console.error("Bot error:", error);

      client.on(RTVIEvent.TransportStateChanged, onStateChange);
      client.on(RTVIEvent.BotOutput, onBotOutput);
      client.on(RTVIEvent.UserTranscript, onUserTranscript);
      client.on(RTVIEvent.Error, onError);

      return () => {
        client.off(RTVIEvent.TransportStateChanged, onStateChange);
        client.off(RTVIEvent.BotOutput, onBotOutput);
        client.off(RTVIEvent.UserTranscript, onUserTranscript);
        client.off(RTVIEvent.Error, onError);
      };
    }, []);

    const isConnected = transportState === "ready";
    const isConnecting = ["authenticating", "connecting", "connected"].includes(transportState);

    const handlePress = async () => {
      if (isConnected) {
        await client.disconnect();
      } else {
        await client.connect({ webrtcRequestParams: { endpoint: BOT_URL } });
      }
    };

    return (
      <View style={styles.container}>
        <Text style={styles.title}>Pipecat Voice App</Text>
        <View style={styles.controls}>
          <Button
            title={isConnected ? "Disconnect" : isConnecting ? "Connecting…" : "Connect"}
            onPress={handlePress}
            disabled={isConnecting}
          />
          <Text style={styles.status}>{transportState}</Text>
        </View>
        <ScrollView style={styles.messages}>
          {messages.map((msg, i) => (
            <Text key={i} style={styles.message}>
              <Text style={styles.role}>{msg.role === "user" ? "You" : "Bot"}: </Text>
              {msg.text}
            </Text>
          ))}
        </ScrollView>
      </View>
    );
  }

  const styles = StyleSheet.create({
    container: { flex: 1, padding: 40, paddingTop: 60 },
    title: { fontSize: 24, marginBottom: 16 },
    controls: { flexDirection: "row", alignItems: "center", marginBottom: 16, gap: 12 },
    status: { color: "#666" },
    messages: { flex: 1 },
    message: { marginBottom: 8 },
    role: { fontWeight: "bold" },
  });
  ```
</View>

<View title="iOS" icon="apple">
  Add the connect/disconnect logic and delegate implementation to `VoiceClientModel.swift`:

  ```swift theme={null}
  extension VoiceClientModel {
    @MainActor
    func connect() {
      let options = PipecatClientOptions(
        transport: SmallWebRTCTransport(),
        enableMic: true
      )
      client = PipecatClient(options: options)
      client?.delegate = self

      let startParams = APIRequest.init(
        endpoint: URL(string: "http://localhost:7860/api/start")!
      )
      client?.startBotAndConnect(startBotParams: startParams) { [weak self] (result: Result<SmallWebRTCStartBotResult, AsyncExecutionError>) in
        switch result {
        case .failure(let error):
          print("Connection failed: \(error)")
          self?.client = nil
        case .success:
          break
        }
      }
    }

    @MainActor
    func disconnect() {
      client?.disconnect(completion: nil)
      client?.release()
      client = nil
    }
  }

  extension VoiceClientModel: PipecatClientDelegate {
    func onTransportStateChanged(state: TransportState) {
      Task { @MainActor in self.transportState = state }
    }

    func onUserTranscript(data: Transcript) {
      Task { @MainActor in
        if data.final ?? false {
          self.messages.append(Message(role: "user", text: data.text))
        }
      }
    }

    func onBotOutput(data: BotOutputData) {
      Task { @MainActor in
        self.messages.append(Message(role: "bot", text: data.text))
      }
    }

    func onError(message: RTVIMessageInbound) {
      print("Bot error: \(message.data ?? "Unknown")")
    }
  }
  ```

  Then create `ContentView.swift`:

  ```swift theme={null}
  import SwiftUI

  struct ContentView: View {
    @StateObject private var model = VoiceClientModel()

    var isConnected: Bool { model.transportState == .ready }
    var isConnecting: Bool {
      [.authenticating, .connecting, .connected].contains(model.transportState)
    }

    var body: some View {
      VStack(alignment: .leading, spacing: 16) {
        Text("Pipecat Voice App").font(.title)

        HStack {
          Button(isConnected ? "Disconnect" : "Connect") {
            isConnected ? model.disconnect() : model.connect()
          }
          .disabled(isConnecting)

          Text(model.transportState.description).foregroundColor(.secondary)
        }

        ScrollView {
          LazyVStack(alignment: .leading) {
            ForEach(model.messages) { msg in
              HStack(alignment: .top) {
                Text(msg.role == "user" ? "You:" : "Bot:").fontWeight(.bold)
                Text(msg.text)
              }
              .padding(.bottom, 4)
            }
          }
        }

        Spacer()
      }
      .padding(40)
    }
  }
  ```
</View>

## Step 3: Run it

Start your Pipecat bot (if it isn't already running), then start the app:

<View title="React" icon="react">
  ```bash theme={null}
  npm run dev
  ```

  Open `http://localhost:5173`, click **Connect**, allow microphone access, and start talking.
</View>

<View title="JavaScript" icon="js">
  ```bash theme={null}
  npm run dev
  ```

  Open `http://localhost:5173`, click **Connect**, allow microphone access, and start talking.
</View>

<View title="React Native" icon="mobile">
  ```bash theme={null}
  npx expo prebuild --clean
  npm run ios -- --device
  # or
  npm run android -- --device
  ```

  The `--device` flag prompts you to choose between connected physical devices and simulators, and starts Metro automatically. Running from Xcode directly won't work — it builds the app but doesn't start Metro, so the app will show "No development servers found."

  **Networking note:** The bot URL differs by target:

  | Target                   | URL                                                                           |
  | ------------------------ | ----------------------------------------------------------------------------- |
  | iOS simulator            | `http://localhost:7860/api/offer`                                             |
  | Android emulator         | `http://10.0.2.2:7860/api/offer`                                              |
  | Physical device (either) | `http://<your-mac-ip>:7860/api/offer` — find it with `ipconfig getifaddr en0` |

  The `BOT_URL` constant in the code above handles the simulator cases automatically.

  Tap **Connect**, allow microphone access, and start talking.
</View>

<View title="iOS" icon="apple">
  In Xcode, select your target device or simulator and press **Run** (⌘R). The app connects to your bot at `http://localhost:7860/api/start` — make sure your bot server is running first.

  <Note>
    On a physical device, `localhost` won't resolve to your Mac. Replace the URL
    in `connect()` with your Mac's local network IP: `ipconfig getifaddr en0`.
  </Note>

  Tap **Connect**, allow microphone access, and start talking.
</View>

## What each piece does

<View title="React" icon="react">
  | Piece                            | Role                                                   |
  | -------------------------------- | ------------------------------------------------------ |
  | `PipecatClient`                  | Manages the connection and session lifecycle           |
  | `SmallWebRTCTransport`           | Handles the WebRTC peer connection to your bot         |
  | `PipecatClientProvider`          | Shares the client instance via React context           |
  | `PipecatClientAudio`             | Mounts a hidden `<audio>` element for bot voice output |
  | `usePipecatClientTransportState` | Tracks connection state for button and status display  |
  | `usePipecatConversation`         | Streams the live transcript of the conversation        |
  | `useRTVIClientEvent`             | Subscribes to a specific event — here, errors          |
</View>

<View title="JavaScript" icon="js">
  | Piece                         | Role                                                 |
  | ----------------------------- | ---------------------------------------------------- |
  | `PipecatClient`               | Manages the connection and session lifecycle         |
  | `SmallWebRTCTransport`        | Handles the WebRTC peer connection to your bot       |
  | `TrackStarted` event          | Notifies when the bot's audio track is ready to play |
  | `TransportStateChanged` event | Drives button state and status display               |
  | `BotOutput` event             | Streams the bot's response text sentence by sentence |
  | `UserTranscript` event        | Receives finalized speech-to-text from the user      |
</View>

<View title="React Native" icon="mobile">
  | Piece                            | Role                                                           |
  | -------------------------------- | -------------------------------------------------------------- |
  | `PipecatClient`                  | Manages the connection and session lifecycle                   |
  | `RNSmallWebRTCTransport`         | Handles the WebRTC peer connection to your bot                 |
  | `DailyMediaManager`              | Manages audio routing and media permissions on the device      |
  | `useEffect` + `.on()` / `.off()` | Subscribes to events and cleans up when the component unmounts |
  | `TransportStateChanged` event    | Drives button state and status display                         |
  | `BotOutput` event                | Streams the bot's response text sentence by sentence           |
  | `UserTranscript` event           | Receives finalized speech-to-text from the user                |
</View>

<View title="iOS" icon="apple">
  | Piece                     | Role                                                            |
  | ------------------------- | --------------------------------------------------------------- |
  | `PipecatClient`           | Manages the connection and session lifecycle                    |
  | `SmallWebRTCTransport`    | Handles the WebRTC peer connection to your bot                  |
  | `PipecatClientDelegate`   | Protocol for receiving all events from the client               |
  | `Task { @MainActor in }`  | Dispatches delegate callbacks to the main thread for UI updates |
  | `onTransportStateChanged` | Drives button state and status display                          |
  | `onBotOutput`             | Receives the bot's text output                                  |
  | `onUserTranscript`        | Receives finalized speech-to-text from the user                 |
</View>

## Next steps

<CardGroup cols={2}>
  <Card title="Session Lifecycle" icon="arrows-rotate" href="/client/concepts/session-lifecycle">
    Understand connection states, reconnection, and teardown
  </Card>

  <Card title="Events & Callbacks" icon="bell" href="/client/concepts/events-and-callbacks">
    The full event system — what fires, when, and how to handle it
  </Card>

  <Card title="Custom Messaging" icon="comments" href="/client/guides/custom-messaging">
    Send and receive custom messages between client and bot
  </Card>

  <Card title="React SDK Reference" icon="react" href="/api-reference/client/react/overview">
    All hooks, components, and types
  </Card>
</CardGroup>
