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.
- 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); - 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"] } } } - 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); - 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.
How to add a voice agent to your website
Make an agent in Crackle’s studio, switch on its call button and paste one line of HTML before </body>. Visitors press it and talk to your agent, with no server of your own.
Read the guideHow to build a voice agent in Next.js
A route handler that starts a Crackle call with your API key, and a client component that joins it with livekit-client. Two files; the key never reaches the browser.
Read the guideHow to build a Hindi voice bot
Create a Crackle agent in Hindi or Hinglish, pick a voice recorded in Hindi, write its instructions the way it should speak, and let it follow callers into English and back.
Read the guideGive 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.