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=falseinandroid/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/react-native-webrtc@118.0.3-daily.4- Update the
platformin yourPodfile, since@daily-co/react-native-webrtconly 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-permissionandroid:name="android.permission.CAMERA" />
<uses-featureandroid:name="android.hardware.camera" />
<uses-featureandroid:name="android.hardware.camera.autofocus"/>
<uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permissionandroid:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permissionandroid:name="android.permission.RECORD_AUDIO" />
<uses-permissionandroid:name="android.permission.WAKE_LOCK" />
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>- Update build.gradle - Set minimum SDK in your top-level
build.gradlefile:
minSdkVersion =24newArchEnabled=false(If you run into any issues, refer to Github issues like these, or the react-native-webrtcinstallation 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).
importVapifrom'@vapi-ai/react-native';// Initialize with your API keyconstvapi=newVapi('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 eventsvapi.on('video',(track)=>{console.log('Video track received:',track);});vapi.on('camera-error',(error)=>{console.error('Camera error:',error);});try{awaitvapi.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/unmutevapi.setMuted(true);// Send a messagevapi.send({type: 'add-message',message: {role: 'user',content: 'Hello from React Native!',},});// End the callvapi.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:
importReactfrom'react';import{View,StyleSheet}from'react-native';import{DailyMediaView}from'@daily-co/react-native-daily-js';constVideoCall=()=>{constparticipants=vapi.participants();return(<Viewstyle={styles.container}>{Object.entries(participants).map(([sessionId,participant])=>(<DailyMediaViewkey={sessionId}videoTrack={participant.tracks.video.track}audioTrack={participant.tracks.audio.track}mirror={participant.local}zOrder={participant.local ? 1 : 0}style={styles.video}/>))}</View>);};conststyles=StyleSheet.create({container: {flex: 1,backgroundColor: 'black',},video: {flex: 1,margin: 5,},});importReact,{useState,useEffect}from'react';import{View,Button,Text}from'react-native';importVapifrom'@vapi-ai/react-native';import{DailyMediaView}from'@daily-co/react-native-daily-js';constVideoCallScreen=()=>{const[vapi]=useState(newVapi('your-api-key'));const[isInCall,setIsInCall]=useState(false);const[participants,setParticipants]=useState({});const[isVideoOn,setIsVideoOn]=useState(true);useEffect(()=>{// Set up event listenersvapi.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 updatedsetParticipants(vapi.participants());});return()=>{vapi.stop();};},[]);conststartCall=async()=>{awaitvapi.start({model: {provider: 'openai',model: 'gpt-4o',messages: [{role: 'system',content: 'You are a helpful video assistant.',}],},voice: {provider: 'tavus',// For video avatarsvoiceId: 'your-voice-id',},});};consttoggleVideo=()=>{vapi.setLocalVideo(!isVideoOn);setIsVideoOn(!isVideoOn);};return(<Viewstyle={{flex: 1}}>{!isInCall ? (<Buttontitle="Start Video Call"onPress={startCall}/>) : (<><Viewstyle={{flex: 1}}>{Object.entries(participants).map(([id,participant])=>(<DailyMediaViewkey={id}videoTrack={participant.tracks?.video?.track}audioTrack={participant.tracks?.audio?.track}mirror={participant.local}style={{flex: 1}}/>))}</View><Viewstyle={{flexDirection: 'row',padding: 20}}><Buttontitle={isVideoOn ? "Turn Off Video" : "Turn On Video"}onPress={toggleVideo}/><Buttontitle="Switch Camera"onPress={()=>vapi.cycleCamera()}/><Buttontitle="End Call"onPress={()=>vapi.stop()}/></View></>)}</View>);};// Enable/disable videovapi.setLocalVideo(true);// Turn on cameravapi.setLocalVideo(false);// Turn off camera// Check if video is enabledconstisVideoOn=vapi.isVideoEnabled();// Switch between front/back cameravapi.cycleCamera();// Start camera before callawaitvapi.startCamera();// Display video tracks using DailyMediaViewimport{DailyMediaView}from'@daily-co/react-native-daily-js';// In your component<DailyMediaViewvideoTrack={participant.videoTrack}audioTrack={participant.audioTrack}mirror={participant.local}zOrder={participant.local ? 1 : 0}style={styles.video}/>// Have the assistant say somethingvapi.say('Hello, how can I help you today?');// With optionsvapi.say('Goodbye!',true,// endCallAfterSpokenfalse,// interruptionsEnabledfalse// interruptAssistantEnabled);// Audio devicesconstaudioDevices=vapi.getAudioDevices();vapi.setAudioDevice(deviceId);constcurrentAudioDevice=vapi.getCurrentAudioDevice();// Camera devicesconstcameraDevices=vapi.getCameraDevices();vapi.setCamera(deviceId);constcurrentCamera=vapi.getCurrentCameraDevice();// Start screen sharingvapi.startScreenShare();// Stop screen sharingvapi.stopScreenShare();// Get all participantsconstparticipants=vapi.participants();// Update participant settingsvapi.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 errorsAlert.alert('Permission Required','Please grant microphone and camera permissions');}elseif(error.code==='NETWORK_ERROR'){// Handle network errorsAlert.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/react-native-webrtc@118.0.3-daily.3# Clean and reinstallcd ios && rm -rf Pods Podfile.lock && pod install &&cd ..# Reset Metro cache
npx react-native start --reset-cache# Disable TurboModulescd android &&echo'newArchEnabled=false'>> gradle.properties &&cd ..# Clean iOS buildcd ios && xcodebuild clean &&cd ..
# Clean Android buildcd android && ./gradlew clean &&cd ..
# Reinstall dependencies
rm -rf node_modules && npm cache verify && npm installWarning: 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/react-native-webrtc@118.0.3-daily.4- 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:androidnewVapi(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