Guide

How to add a voice agent to a React Native app

Your server starts a call with one request to Crackle’s API and returns a join token. Your Expo or React Native app joins with LiveKit’s React Native SDK, publishes the microphone and plays the agent. It needs a development build rather than Expo Go, because calls use native audio code.

What you need.

  • An agent and its ID, and a workspace API key (ck_…), both from the studio.
  • An Expo app you can make a development build of.

Step by step.

  1. Start calls from your backendThe API key belongs on your server, never in the app. A small route starts the call and returns what the app needs to join.

    server.js

    import express from 'express';
    
    const app = express();
    
    // Your app asks your server for a call; your server asks Crackle.
    app.post('/api/call', async (req, res) => {
      const response = await fetch('https://goblins-call-api.onrender.com/v1/calls', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.CRACKLE_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ agentId: process.env.CRACKLE_AGENT_ID }),
      });
      if (!response.ok) return res.status(502).json(await response.json());
    
      // Only what the app needs to join. The API key stays here.
      const { callId, livekitUrl, token } = await response.json();
      res.json({ callId, livekitUrl, token });
    });
    
    app.listen(3000);
  2. Install LiveKit’s packagesAdd the config plugins and the microphone permission to app.json, then make a development build.

    Terminal

    npx expo install @livekit/react-native @livekit/react-native-webrtc livekit-client \
      @livekit/react-native-expo-plugin @config-plugins/react-native-webrtc expo-keep-awake

    app.json

    {
      "expo": {
        "plugins": ["@livekit/react-native-expo-plugin", "@config-plugins/react-native-webrtc"],
        "ios": { "infoPlist": { "NSMicrophoneUsageDescription": "Allow the microphone so you can talk on a call." } },
        "android": { "permissions": ["android.permission.RECORD_AUDIO"] }
      }
    }
  3. Register WebRTC onceIn the app’s entry file, before anything creates a room.

    index.js

    import { registerGlobals } from '@livekit/react-native';
    import { registerRootComponent } from 'expo';
    import App from './App';
    
    registerGlobals();
    registerRootComponent(App);
  4. Build the call screenIt starts the phone’s audio session, asks your backend for a call and hands the answer to LiveKitRoom, which connects, sends the microphone and plays the agent.

    CallScreen.jsx

    import { useEffect, useState } from 'react';
    import { Button, Text, View } from 'react-native';
    import { AudioSession, LiveKitRoom, useRoomContext } from '@livekit/react-native';
    import { useKeepAwake } from 'expo-keep-awake';
    
    export default function CallScreen({ onDone }) {
      const [session, setSession] = useState(null);
      const [error, setError] = useState('');
      useKeepAwake(); // a call that stops when the screen dims is a dropped call
    
      useEffect(() => {
        let cancelled = false;
        (async () => {
          try {
            await AudioSession.startAudioSession();
            // Your own backend, which calls POST /v1/calls with the API key.
            const response = await fetch('https://your-app.com/api/call', { method: 'POST' });
            if (!response.ok) throw new Error('The call could not be started.');
            const body = await response.json();
            if (!cancelled) setSession(body);
          } catch (err) {
            if (!cancelled) setError(err.message);
          }
        })();
        return () => {
          cancelled = true;
          AudioSession.stopAudioSession();
        };
      }, []);
    
      if (error) return <Text>{error}</Text>;
      if (!session) return <Text>Connecting…</Text>;
    
      return (
        <LiveKitRoom serverUrl={session.livekitUrl} token={session.token} connect audio video={false} onDisconnected={onDone}>
          <InCall />
        </LiveKitRoom>
      );
    }
    
    function InCall() {
      const room = useRoomContext();
      const [muted, setMuted] = useState(false);
    
      const toggleMute = async () => {
        await room.localParticipant.setMicrophoneEnabled(muted);
        setMuted(!muted);
      };
    
      return (
        <View>
          <Button title={muted ? 'Unmute' : 'Mute'} onPress={toggleMute} />
          <Button title="Hang up" onPress={() => room.disconnect()} />
        </View>
      );
    }

All of it, in the docs.

The worked example has every file, and the guide explains each part.

Questions.

Does it run in Expo Go?

No. Calls use native audio code, so the app needs a development build (npx expo run:ios or run:android).

Does it work in a bare React Native app?

Yes. Install the same packages and do the native setup LiveKit’s own React Native guide describes; the call screen is the same.

Can the user interrupt the agent?

Yes. When the user starts talking the agent stops mid-word and listens, and a short “okay” is not taken as an interruption.

Other guides.

Give your agent a voice today.

Build one in the studio and talk to it in your browser. Sign-up credit is there for your first test calls.