This package lets you start Vapi voice and video calls directly in your React Native app.
β οΈ Important Notes- π Prerequisites
- π§ Installation
- π Troubleshooting
- π Expo Integration
- π API Reference
- π€ Contributing
- π License
- π Support
- This guide is for bare React Native projects. If you use expo build, skip to the Expo Integration
- Requires React Native 0.60+ for autolinking support
- Not compatible with Expo Go - requires custom native code
- Not compatible with TurboModules - requires
newArchEnabled=false
inandroid/gradle.properties
- React Native 0.60+
- Node.js 20+
- React Native CLI or @react-native-community/cli
- Xcode 14+ (for iOS development)
- Android Studio (for Android development)
- CocoaPods (for iOS dependencies)
- Minimum iOS version: 12.0
- Screen sharing: Requires iOS 14.0+
- Minimum SDK: 24
newArchEnabled
in your android/gradle.properties
file.
Install @vapi-ai/react-native
along with its peer dependencies:
# Install main dependencies
npm install @vapi-ai/react-native @daily-co/react-native-daily-js @react-native-async-storage/async-storage react-native-background-timer react-native-get-random-values
# Install exact WebRTC version (important for compatibility)
npm install --save-exact @daily-co/[email protected]
- Update the
platform
in yourPodfile
, since@daily-co/react-native-webrtc
only works on iOS 12 and above:
platform :ios, '12.0'
Note: If you wish to send screen share from iOS, it only works on iOS 14 and above. In this case, use iOS 14.0 instead of iOS 12.0.
- Install CocoaPods dependencies:
npx pod-install
- Configure Info.plist (required to prevent crashes):
Add these keys to your ios/YourApp/Info.plist
:
Key | Type | Value |
---|---|---|
NSCameraUsageDescription |
String | "This app needs camera access for video calls" |
NSMicrophoneUsageDescription |
String | "This app needs microphone access for voice calls" |
UIBackgroundModes |
Array | Item 0: voip |
UIBackgroundModes
is handled slightly differently and will resolve to an array. For its first item, specify the value voip
. This ensures that audio will continue uninterrupted when your app is sent to the background.
To add the new rows through Xcode, open the Info.plist
and add the following three rows:
Key | Type | Value |
---|---|---|
Privacy - Camera Usage Description | String | "Vapi Playground needs camera access to work" |
Privacy - Microphone Usage Description | String | "Vapi Playground needs microphone access to work" |
Required background modes | Array | 1 item |
---> Item 0 | String | "App provides Voice over IP services" |
If you view the raw file contents of Info.plist
, it should look like this:
<dict>
...
<key>NSCameraUsageDescription</key>
<string>Vapi Playground needs camera access to work</string>
<key>NSMicrophoneUsageDescription</key>
<string>Vapi Playground needs microphone access to work</string>
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
</array>
</dict>
- Update AndroidManifest.xml - Add permissions:
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- Update build.gradle - Set minimum SDK in your top-level
build.gradle
file:
minSdkVersion = 24
newArchEnabled=false
(If you run into any issues, refer to Github issues like these, or the react-native-webrtc
installation docs, which walk you through a more complicated process. The simpler process laid out above seems to work in a vanilla modern React Native CLI-based setup).
import Vapi from '@vapi-ai/react-native';
// Initialize with your API key
const vapi = new Vapi('your-vapi-api-key');
vapi.on('call-start', () => {
console.log('Call started');
});
vapi.on('call-end', () => {
console.log('Call ended');
});
vapi.on('volume-level', (volume) => {
console.log('Volume level:', volume);
});
vapi.on('error', (error) => {
console.error('Vapi error:', error);
});
// Video-specific events
vapi.on('video', (track) => {
console.log('Video track received:', track);
});
vapi.on('camera-error', (error) => {
console.error('Camera error:', error);
});
try {
await vapi.start({
model: {
provider: 'openai',
model: 'gpt-4o',
messages: [
{
role: 'system',
content: 'You are a helpful AI assistant.',
},
],
},
voice: {
provider: '11labs',
voiceId: 'pNInz6obpgDQGcFmaJgB',
},
firstMessage: 'Hello! How can I help you today?',
});
} catch (error) {
console.error('Failed to start call:', error);
}
// Mute/unmute
vapi.setMuted(true);
// Send a message
vapi.send({
type: 'add-message',
message: {
role: 'user',
content: 'Hello from React Native!',
},
});
// End the call
vapi.stop();
The SDK now fully supports video calls with the following capabilities:
To display video, you'll need to use the DailyMediaView
component from @daily-co/react-native-daily-js
:
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { DailyMediaView } from '@daily-co/react-native-daily-js';
const VideoCall = () => {
const participants = vapi.participants();
return (
<View style={styles.container}>
{Object.entries(participants).map(([sessionId, participant]) => (
<DailyMediaView
key={sessionId}
videoTrack={participant.tracks.video.track}
audioTrack={participant.tracks.audio.track}
mirror={participant.local}
zOrder={participant.local ? 1 : 0}
style={styles.video}
/>
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'black',
},
video: {
flex: 1,
margin: 5,
},
});
import React, { useState, useEffect } from 'react';
import { View, Button, Text } from 'react-native';
import Vapi from '@vapi-ai/react-native';
import { DailyMediaView } from '@daily-co/react-native-daily-js';
const VideoCallScreen = () => {
const [vapi] = useState(new Vapi('your-api-key'));
const [isInCall, setIsInCall] = useState(false);
const [participants, setParticipants] = useState({});
const [isVideoOn, setIsVideoOn] = useState(true);
useEffect(() => {
// Set up event listeners
vapi.on('call-start', () => {
setIsInCall(true);
});
vapi.on('call-end', () => {
setIsInCall(false);
setParticipants({});
});
vapi.on('daily-participant-updated', () => {
setParticipants(vapi.participants());
});
vapi.on('video', (track) => {
// Video track received, participants will be updated
setParticipants(vapi.participants());
});
return () => {
vapi.stop();
};
}, []);
const startCall = async () => {
await vapi.start({
model: {
provider: 'openai',
model: 'gpt-4o',
messages: [{
role: 'system',
content: 'You are a helpful video assistant.',
}],
},
voice: {
provider: 'tavus', // For video avatars
voiceId: 'your-voice-id',
},
});
};
const toggleVideo = () => {
vapi.setLocalVideo(!isVideoOn);
setIsVideoOn(!isVideoOn);
};
return (
<View style={{ flex: 1 }}>
{!isInCall ? (
<Button title="Start Video Call" onPress={startCall} />
) : (
<>
<View style={{ flex: 1 }}>
{Object.entries(participants).map(([id, participant]) => (
<DailyMediaView
key={id}
videoTrack={participant.tracks?.video?.track}
audioTrack={participant.tracks?.audio?.track}
mirror={participant.local}
style={{ flex: 1 }}
/>
))}
</View>
<View style={{ flexDirection: 'row', padding: 20 }}>
<Button
title={isVideoOn ? "Turn Off Video" : "Turn On Video"}
onPress={toggleVideo}
/>
<Button title="Switch Camera" onPress={() => vapi.cycleCamera()} />
<Button title="End Call" onPress={() => vapi.stop()} />
</View>
</>
)}
</View>
);
};
// Enable/disable video
vapi.setLocalVideo(true); // Turn on camera
vapi.setLocalVideo(false); // Turn off camera
// Check if video is enabled
const isVideoOn = vapi.isVideoEnabled();
// Switch between front/back camera
vapi.cycleCamera();
// Start camera before call
await vapi.startCamera();
// Display video tracks using DailyMediaView
import { DailyMediaView } from '@daily-co/react-native-daily-js';
// In your component
<DailyMediaView
videoTrack={participant.videoTrack}
audioTrack={participant.audioTrack}
mirror={participant.local}
zOrder={participant.local ? 1 : 0}
style={styles.video}
/>
// Have the assistant say something
vapi.say('Hello, how can I help you today?');
// With options
vapi.say(
'Goodbye!',
true, // endCallAfterSpoken
false, // interruptionsEnabled
false // interruptAssistantEnabled
);
// Audio devices
const audioDevices = vapi.getAudioDevices();
vapi.setAudioDevice(deviceId);
const currentAudioDevice = vapi.getCurrentAudioDevice();
// Camera devices
const cameraDevices = vapi.getCameraDevices();
vapi.setCamera(deviceId);
const currentCamera = vapi.getCurrentCameraDevice();
// Start screen sharing
vapi.startScreenShare();
// Stop screen sharing
vapi.stopScreenShare();
// Get all participants
const participants = vapi.participants();
// Update participant settings
vapi.updateParticipant(sessionId, {
setSubscribedTracks: {
audio: true,
video: true
}
});
vapi.on('network-quality-change', (event) => {
console.log('Network quality:', event);
});
vapi.on('network-connection', (event) => {
console.log('Network connection:', event);
});
vapi.on('call-start-progress', (event) => {
console.log(`Stage: ${event.stage}, Status: ${event.status}`);
});
vapi.on('call-start-success', (event) => {
console.log('Call started successfully in', event.totalDuration, 'ms');
});
vapi.on('call-start-failed', (event) => {
console.error('Call failed at stage:', event.stage, 'Error:', event.error);
});
vapi.on('error', (error) => {
if (error.code === 'PERMISSION_DENIED') {
// Handle permission errors
Alert.alert('Permission Required', 'Please grant microphone and camera permissions');
} else if (error.code === 'NETWORK_ERROR') {
// Handle network errors
Alert.alert('Network Error', 'Please check your internet connection');
}
});
vapi.on('camera-error', (error) => {
console.error('Camera error:', error);
Alert.alert('Camera Error', 'Unable to access camera');
});
- Ensure you've added the required permissions to Info.plist/AndroidManifest.xml
- Request permissions at runtime on Android
# If you see version conflicts, use the exact version:
npm install --save-exact @daily-co/[email protected]
# Clean and reinstall
cd ios && rm -rf Pods Podfile.lock && pod install && cd ..
# Reset Metro cache
npx react-native start --reset-cache
# Disable TurboModules
cd android && echo 'newArchEnabled=false' >> gradle.properties && cd ..
# Clean iOS build
cd ios && xcodebuild clean && cd ..
# Clean Android build
cd android && ./gradlew clean && cd ..
# Reinstall dependencies
rm -rf node_modules && npm cache verify && npm install
Warning: This SDK cannot be used with Expo Go. Use Expo Development Build or EAS Build.
- Install dependencies:
npx expo install @vapi-ai/react-native @daily-co/react-native-daily-js @react-native-async-storage/async-storage react-native-background-timer react-native-get-random-values
npm install --save-exact @daily-co/[email protected]
- Update app.json:
{
"expo": {
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.anonymous.ReactNativeWithExpo52",
"infoPlist": {
"NSCameraUsageDescription": "Vapi Playground needs camera access to work",
"NSMicrophoneUsageDescription": "Vapi Playground needs microphone access to work",
"UIBackgroundModes": ["voip"]
}
},
"android": {
"permissions": [
"android.permission.RECORD_AUDIO",
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.CAMERA",
"android.permission.SYSTEM_ALERT_WINDOW",
"android.permission.WAKE_LOCK",
"android.permission.BLUETOOTH",
"android.permission.FOREGROUND_SERVICE",
"android.permission.FOREGROUND_SERVICE_CAMERA",
"android.permission.FOREGROUND_SERVICE_MICROPHONE",
"android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION",
"android.permission.POST_NOTIFICATIONS"
],
},
"plugins": [
"@config-plugins/react-native-webrtc",
"@daily-co/config-plugin-rn-daily-js",
[
"expo-build-properties",
{
"android": {
"minSdkVersion": 24
},
"ios": {
"deploymentTarget": "15.1"
}
}
]
]
}
}
- Prebuild and run:
npx expo prebuild
npx expo run:ios # or run:android
new Vapi(apiKey: string, apiBaseUrl?: string)
start(assistant, overrides?)
- Start a voice/video callstop()
- End the current callsend(message)
- Send a message during callsay(message, endCallAfterSpoken?, interruptionsEnabled?, interruptAssistantEnabled?)
- Have assistant speak
setMuted(muted: boolean)
- Mute/unmute microphoneisMuted()
- Check if currently mutedgetAudioDevices()
- Get available audio devicessetAudioDevice(deviceId)
- Set audio devicegetCurrentAudioDevice()
- Get current audio device
setLocalVideo(enable: boolean)
- Enable/disable videoisVideoEnabled()
- Check if video is enabledstartCamera()
- Start camera before callcycleCamera()
- Switch between camerasgetCameraDevices()
- Get available camerassetCamera(deviceId)
- Set specific cameragetCurrentCameraDevice()
- Get current camera
startScreenShare()
- Start screen sharingstopScreenShare()
- Stop screen sharing
participants()
- Get all participantsupdateParticipant(sessionId, updates)
- Update participant settings
getDailyCallObject()
- Get underlying Daily call objectupdateSendSettings(settings)
- Update send settingsupdateReceiveSettings(settings)
- Update receive settingsupdateInputSettings(settings)
- Update input settings
call-start
- Call has startedcall-end
- Call has endedvolume-level
- Volume level changedspeech-start
- Speech detection startedspeech-end
- Speech detection endedmessage
- Received messageerror
- Error occurredvideo
- Video track receivedcamera-error
- Camera error occurrednetwork-quality-change
- Network quality changednetwork-connection
- Network connection statusdaily-participant-updated
- Participant updatedcall-start-progress
- Call initialization progresscall-start-success
- Call started successfullycall-start-failed
- Call failed to start
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
MIT License Copyright (c) 2023 Vapi Labs Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- Documentation: Vapi Docs
- Issues: GitHub Issues
- Discord: Vapi Community