Build multi-modal AI applications using our new open-source Vision AI SDK.

React Native Video Calling Tutorial

Build a Zoom-style video calling app for iOS and Android with Stream's React Native Video SDK. Get two people on a call, then swap in the prebuilt call UI, controls, and theming.

Prefer to skip the setup? Add the Stream skill and let your AI agent build your React Native video calling app.

This tutorial teaches you how to build a Zoom/Whatsapp-style video calling app.

  • All calls run on Stream's global edge network for optimal latency & reliability.
  • Configuring permissions gives you fine-grained control over who can do what.
  • Video quality and codecs are automatically optimized.
  • The calling support is powered by Stream's Video Calling API.

Preview of the video view

This page works for both humans and AI coding agents: every step is a file operation or a shell command, every step has a verification checkpoint, and the few steps that need a human are explicitly marked.

Before you start. Emulators have no camera, so use a physical device; the first iOS device build also needs a signing team. You won't need a second phone - Step 6 lets you join the call from a browser tab.

Choose your path

This tutorial has two paths. In Path A you let your AI agent build the app for you. In Path B you build it yourself, step by step. Both paths end with the same working video call, so pick the one you prefer instead of working through both.

Install the Stream CLI once, then add the skills. This gives Claude Code, Cursor, or Codex the React Native integration patterns and current SDK APIs, so it builds against real docs instead of stale training data.

Terminal (bash)
1
2
3
4
5
6
7
# Install the getstream CLI curl -fsSL https://getstream.io/cli.sh | bash # Install the skills (router, docs, and builder). The React Native pack # installs on demand the first time it's needed, or add it explicitly: getstream skills getstream skills stream-react-native

Then ask your agent:

Prompt (markdown)
1
2
/stream-react-native Build a React Native video calling app with the prebuilt call UI and call controls.

Where you come in. The agent handles credentials and code. It stops three times: getstream init opening your browser to sign in and pick an app (new accounts get an organization in that flow), the build to a device, and the camera and microphone prompt.

Want to get the sign-in out of the way first? Run getstream init before you prompt the agent - it picks up the initialized project from there.

Human checkpoint: build the app to a device if the agent hasn't, then allow camera and microphone access. You should join the call and see your own video.

Skim Important Building Blocks if you want to understand what the agent wrote before you extend it.

Path B - Build it manually

Choose this path if you'd rather write the code yourself, or you want to understand what the SDK is doing before you hand it to an agent. Follow the steps below.

For AI assistants reading this page: append .md to any Stream docs URL for a clean Markdown version. A condensed index for the React Native Video SDK is at https://getstream.io/video/docs/react-native/llms.txt. Prefer these over parsing HTML.

Important Building Blocks

Four pieces, one mental model:

  • StreamVideoClient - the low-level client. Holds the API key, the user, the token and the websocket connection to the Stream Video service.
  • Call - a single call, created with client.call(type, id). call.join() is what actually sets up audio and video.
  • Providers - <StreamVideo client={client} /> puts the client in React context, <StreamCall call={call} /> puts one call in context. Everything below them reads from those.
  • Call state hooks - useCallStateHooks() hands you the hooks (useParticipantCount, useDominantSpeaker, and the rest) that drive the UI.

End to end, that looks like this:

tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { StreamVideoClient } from "@stream-io/video-react-native-sdk"; const apiKey = "REPLACE_WITH_API_KEY"; const user = { id: "REPLACE_WITH_USER_ID", name: "REPLACE_WITH_USER_NAME", image: "REPLACE_WITH_USER_IMAGE", }; const token = "REPLACE_WITH_USER_TOKEN"; const callId = "REPLACE_WITH_CALL_ID"; // create a client instance const client = new StreamVideoClient({ apiKey, user, token }); // Alternatively, you can also choose to separate client creation and user connection: // const client = new StreamVideoClient({ apiKey }); // await client.connectUser(user, token); // create a call instance const call = client.call("default", callId); await call.join({ create: true, // create the call if it doesn't exist data: { members: [{ user_id: "john_smith" }, { user_id: "jane_doe" }], // custom data set on call custom: { title: "React Native test", description: "Conducting a test of React Native video calls", }, }, });

The client is created once, at sign-in. The call is created per call screen. Both are handed to the rest of your app through the two providers:

tsx
1
2
3
4
5
6
7
8
9
10
11
import { StreamVideo, StreamCall } from "@stream-io/video-react-native-sdk"; <StreamVideo client={client}> ... <StreamCall call={call}> <View> <Text>Video Call UI</Text> </View> </StreamCall> ... </StreamVideo>;

You'll build a bare call screen first, confirm it connects, then swap in the prebuilt UI.

Step 1 - Create a New React Native App

First, set up your React Native environment. Then generate a project called "VideoCallExample" with the React Native Community CLI:

Terminal (bash)
1
2
npx @react-native-community/cli@latest init VideoCallExample --pm yarn cd VideoCallExample

The React Native CLI installs the iOS CocoaPods dependencies during init. If you skipped that prompt, or a later iOS build fails to find a pod, (re)install them:

  1. cd ios to navigate to the ios folder
  2. bundle install to install Bundler
  3. bundle exec pod install to install the iOS dependencies managed by CocoaPods

Checkpoint: you have a VideoCallExample directory and yarn ios or yarn android builds the React Native starter screen.

Step 2 - Install the SDK and Declare Permissions

To install the Stream Video React Native SDK, run the following command in your terminal of choice:

Terminal (bash)
1
yarn add @stream-io/video-react-native-sdk @stream-io/react-native-webrtc

The SDK requires installing some peer dependencies. You can run the following command to install them:

Terminal (bash)
1
2
3
4
5
yarn add react-native-svg yarn add @react-native-community/netinfo # Install pods for iOS npx pod-install

Declare Permissions

The video calling app we built in this tutorial requires permission to access the users' camera, microphone, and network state.

Android
iOS

In AndroidManifest.xml add the following permissions before the application section.

xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.audio.output" /> <uses-feature android:name="android.hardware.microphone" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.INTERNET" /> <!-- ... --> <application> <!-- ... --> </application> </manifest>

In android/build.gradle, confirm minSdkVersion is at least 24 inside the buildscript section. React Native 0.86 templates already set this, so there is usually nothing to change:

gradle
1
2
3
4
5
6
7
buildscript { ext { // ... minSdkVersion = 24 } // ... }

Run the App

Emulators have no camera, so run on a physical device (how to). The first iOS device build needs a signing team: in Xcode, under Signing & Capabilities, select your Team and change the bundle identifier if the default is taken.

Human checkpoint: the app builds and launches on your device. Agents: run the build, then ask the human to confirm it launched.

Step 3 - Get Your Credentials

The code in Step 4 needs four values:

  • apiKey identifies your Stream Video application to our servers.
  • user is the user object, for example { id: "john_smith", name: "John Smith" }.
  • token authorizes that user. Your server-side API mints it on sign-in; the Client & Authentication guide covers the production shape.
  • callId is anything URL-safe. Calls are created the first time somebody joins with create: true, so there's nothing to provision up front.

There are two ways to get them.

Option 1 - Your own Stream app, via the Stream CLI

The getstream CLI provisions all of it in one flow.

1. Install the CLI (skip if you did this in Path A). This installs globally, so it doesn't matter where you run it:

Terminal (bash)
1
curl -fsSL https://getstream.io/cli.sh | bash

2. Initialize the project. Run this and the remaining commands from the VideoCallExample directory you created in Step 1 - the CLI stores project credentials there. This one command authenticates you, lets you create or select an organization and app, and writes those credentials. New to Stream? The sign-in flow creates your organization. Already have an org or an app? It lets you pick them.

Terminal (bash)
1
getstream init

Human checkpoint: getstream init opens a browser to authenticate. Agents: run it, then ask the human to finish signing in before continuing. It's required first - token fails with "project credentials missing; run getstream init first" until it runs.

3. Mint a user token for a user in your app (never expiring by default; add a TTL for production-like testing):

Terminal (bash)
1
2
getstream token john_smith getstream token john_smith --ttl 1d

The CLI prints your API key alongside the token. Paste both into the constants in Step 4. (getstream env --target ios and --target android can write the key into the native projects instead, but the snippets here read a plain JavaScript constant, so pasting it is the shortest path.)

Checkpoint: you have an API key, a token, the user id you minted it for, and a call id you chose. All four belong to the same app.

Option 2 - Pre-filled tutorial credentials, no account

Want to skip account setup entirely? Every code block below marked with the lock icon is filled in for you with working credentials against Stream's shared tutorial environment. Copy the block as-is and it runs.

Swap in your own credentials from Option 1 before building anything real. Tutorial credentials are shared and short lived.

Step 4 - Setup a Starter UI

Start with a basic UI. A real app would use a navigation library like React Navigation; this tutorial mocks navigation with an activeScreen state variable to stay focused on the SDK.

Create a folder named src and add these files to it:

  • src/HomeScreen.tsx
  • src/CallScreen.tsx (takes callId as a prop)

Now copy the following content into the respective files (as mentioned in the header):

App.tsx
src/CallScreen.tsx
src/HomeScreen.tsx
App.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useState } from "react"; import { StyleSheet } from "react-native"; import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; import { HomeScreen } from "./src/HomeScreen"; import { CallScreen } from "./src/CallScreen"; const apiKey = "REPLACE_WITH_API_KEY"; const token = "REPLACE_WITH_TOKEN"; const userId = "REPLACE_WITH_USER_ID"; const callId = "REPLACE_WITH_CALL_ID"; export default function App() { const [activeScreen, setActiveScreen] = useState("home"); const goToCallScreen = () => setActiveScreen("call-screen"); const goToHomeScreen = () => setActiveScreen("home"); return ( <SafeAreaProvider> <SafeAreaView style={styles.container}> {activeScreen === "call-screen" ? ( <CallScreen goToHomeScreen={goToHomeScreen} callId={callId} /> ) : ( <HomeScreen goToCallScreen={goToCallScreen} /> )} </SafeAreaView> </SafeAreaProvider> ); } const styles = StyleSheet.create({ container: { flex: 1, }, });

Fill in apiKey, userId, token and callId from Step 3. They're hard-coded to keep the tutorial short; in production your backend returns the token at sign-in and the API key comes from environment config.

You should see on your device the following UI.

Preview of the Home Screen Preview of the Home Screen

Preview of empty video UI Preview of empty video UI

Checkpoint: the app shows the home screen with a "Join Video Call" button, and tapping it switches to the placeholder call screen.

Step 5 - Setup the Video Client

Now create the StreamVideoClient and connect the user. For brevity this tutorial creates the client at module scope; in a real app create it inside a useEffect (or memoize it with useState) and call client.disconnectUser() on unmount, so you don't open multiple websockets.

Pass the client to StreamVideo, which shares it with every child component through React Context. It goes at the top of the component tree.

App.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
... import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; import { StreamVideo, StreamVideoClient, } from '@stream-io/video-react-native-sdk'; ... const user = { id: userId, name: 'John Malkovich', image: 'https://robohash.org/John', }; const client = new StreamVideoClient({ apiKey, user, token }); export default function App() { ... return ( <SafeAreaProvider> <StreamVideo client={client}> <SafeAreaView style={styles.container}> ... </SafeAreaView> </StreamVideo> </SafeAreaProvider> ); }

The UI won't change yet, because you haven't joined a call. To confirm the user connected, drop this probe component anywhere inside StreamVideo (it renders the user's id - rendering the object itself throws "Objects are not valid as a React child"):

App.tsx (tsx)
1
2
3
4
5
6
7
import { Text } from 'react-native'; import { useConnectedUser } from '@stream-io/video-react-native-sdk'; const ConnectedUserProbe = () => { const connectedUser = useConnectedUser(); return <Text>Connected as: {connectedUser?.id ?? 'nobody yet'}</Text>; };

Checkpoint: the probe shows your user id rather than nobody yet. Delete it once you've seen it.

Step 6 - Create & Join a Call

Now create and join a call. The call goes into a state variable and gets passed to StreamCall, which provides the hooks that drive the call UI.

Update src/CallScreen.tsx as follows. As in the other steps, ... marks unchanged code - this is a merge, not a whole-file replacement:

src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
... import { Call, StreamCall } from '@stream-io/video-react-native-sdk'; ... export const CallScreen = ({ goToHomeScreen, callId }: Props) => { const [call, setCall] = React.useState<Call | null>(null); if (!call) { return ( <View style={styles.container}> <Text style={styles.text}>Joining call...</Text> </View> ); } return ( <StreamCall call={call}> <View style={styles.container}> <Text style={styles.text}>Here we will add Video Calling UI</Text> <Button title="Go back" onPress={goToHomeScreen} /> </View> </StreamCall> ); };

As covered in Important Building Blocks, a call is created or accessed with client.call(...), so CallScreen needs the client. The useStreamVideoClient hook returns it.

Put the joining logic in a useEffect so the call is joined automatically when the user lands on CallScreen.

src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import React, {useEffect} from 'react'; import { ... useStreamVideoClient, useCallStateHooks, CallingState, } from '@stream-io/video-react-native-sdk'; export const CallScreen = ({goToHomeScreen, callId}: Props) => { const [call, setCall] = React.useState<Call | null>(null); const client = useStreamVideoClient(); useEffect(() => { const _call = client?.call('default', callId); _call?.join({ create: true }) .then(() => setCall(_call)) .catch((error) => console.error('Failed to join the call', error)); }, [client, callId]); useEffect(() => { return () => { // cleanup the call on unmount if the call was not left already if (call?.state.callingState !== CallingState.LEFT) { call?.leave(); } }; }, [call]); if (!call) { return ( <View style={styles.container}> <Text style={styles.text}>Joining call...</Text> </View> ); } return ( <StreamCall call={call}> <View style={styles.container}> <Text style={styles.text}>Here we will add Video Calling UI</Text> <Button title="Go back" onPress={goToHomeScreen} /> <ParticipantCountText /> </View> </StreamCall> ); }; const ParticipantCountText = () => { const {useParticipantCount} = useCallStateHooks(); const participantCount = useParticipantCount(); return ( <Text style={styles.text}>Call has {participantCount} participants</Text> ); };

StreamCall is what makes the call state available to everything below it. useCallStateHooks() returns the individual state hooks - useParticipantCount above, and many more - and each one re-renders your component when that piece of state changes.

You don't need a second phone to test this. Join the same call from your browser:

  1. Refresh the app and tap "Join Video Call".
  2. Click "Join Call" below to open the same call in a browser tab.
  3. The browser opens a lobby screen first - click Join there too. The count only moves after that second click, and the browser signs you in under a generated guest name rather than the user id in the link.
For testing you can join the call on our web-app:

The mobile app updates to Call has 2 participants. Keep that browser tab open for the rest of the tutorial.

Checkpoint: the participant count reads 2. The button joins the shared tutorial call, so on your own credentials from Option 1, open a second browser profile against your own app with a different user id and the same call id.

Step 7 - Render the Video Calling UI

Now add the participant view, which renders each participant's video and audio along with the controls for muting, switching camera, and hanging up.

CallContent handles all of this for you:

  • Indicators to visualize when someone is speaking.
  • The quality of call participants' network.
  • Layout support for multiple participants.
  • Labels for the participants' names and the media stream's on/off status.
  • A floating local video view.
  • Buttons to toggle audio/video and to flip the camera.
  • A button to end/terminate the call.

Update the CallScreen component as follows. The three red lines are being replaced by CallContent - delete them, along with the now-unused Button import and the ParticipantCountText component at the bottom of the file, or yarn lint will flag them later:

src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
-
13
-
14
-
15
+
16
+
17
+
18
19
20
21
22
23
... import { ... CallContent, } from '@stream-io/video-react-native-sdk'; ... export const CallScreen = ({ goToHomeScreen, callId }: Props) => { ... return ( <StreamCall call={call}> <View style={styles.container}> <Text style={styles.text}>Here we will add Video Calling UI</Text> <Button title="Go back" onPress={goToHomeScreen} /> <ParticipantCountText /> <CallContent onHangupCallHandler={goToHomeScreen} /> </View> </StreamCall> ); }; ...

Run the app and you'll see your own video in a floating tile, the video from your browser tab behind it, and the control buttons along the bottom:

Preview of the video view

Human checkpoint: the device asks for camera and microphone access the first time - allow both. Agents: build and launch, then ask the human to grant access and confirm both video streams render.

This is the payoff: a working two-way video call. On the shared tutorial credentials? Create your own Stream app - the free maker plan covers hobby projects, and Step 3 swaps the credentials over in two commands.

Step 8 - Customize the Calling UI

Three ways to customize, in increasing order of effort: theming for colors and fonts, mixing your own components in alongside Stream's, or building the tiles and controls from scratch.

Start by passing your own component to CallContent:

src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
... import { ... StreamCall, CallControlProps, HangUpCallButton, ToggleAudioPublishingButton as ToggleMic, ToggleVideoPublishingButton as ToggleCamera, useCall, useStreamVideoClient, } from '@stream-io/video-react-native-sdk'; const CustomCallControls = (props: CallControlProps) => { const call = useCall(); return ( <View style={styles.customCallControlsContainer}> <ToggleMic onPressHandler={() => call?.microphone.toggle()} /> <ToggleCamera onPressHandler={() => call?.camera.toggle()} /> <HangUpCallButton onHangupCallHandler={props.onHangupCallHandler} /> </View> ); }; ... export const CallScreen = ({goToHomeScreen, callId}: Props) => { ... return ( <StreamCall call={call}> <View style={styles.container}> <CallContent onHangupCallHandler={goToHomeScreen} CallControls={CustomCallControls} /> </View> </StreamCall> ); }; const styles = StyleSheet.create({ ... customCallControlsContainer: { position: 'absolute', bottom: 40, paddingVertical: 10, width: '80%', marginHorizontal: 20, flexDirection: 'row', alignSelf: 'center', justifyContent: 'space-around', backgroundColor: 'orange', borderRadius: 10, borderColor: 'black', borderWidth: 5, zIndex: 5, }, });

Preview of the video view

Next, combine CallContent with a custom top bar that reads call state hooks to show:

  • the participants currently in the call
  • the name of the dominant speaker
src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { ... useStreamVideoClient, useCallStateHooks, } from '@stream-io/video-react-native-sdk'; ... const CustomTopView = () => { const {useParticipants, useDominantSpeaker} = useCallStateHooks(); const participants = useParticipants(); const dominantSpeaker = useDominantSpeaker(); return ( <View style={styles.topContainer}> <Text ellipsizeMode="tail" numberOfLines={1} style={styles.topText}> Video Call between {participants.map(p => p.name).join(', ')} </Text> {dominantSpeaker?.name && ( <Text style={styles.topText}> Dominant Speaker: {dominantSpeaker?.name} </Text> )} </View> ); }; export const CallScreen = ({goToHomeScreen, callId}: Props) => { ... return ( <StreamCall call={call}> <View style={styles.container}> <CustomTopView/> <CallContent onHangupCallHandler={goToHomeScreen} CallControls={CustomCallControls} /> </View> </StreamCall> ); }; const styles = StyleSheet.create({ ... topContainer: { width: '100%', height: 50, backgroundColor: 'black', justifyContent: 'center', alignItems: 'center', }, topText: { color: 'white', }, });

Preview of the video view

For colors, fonts and icons, pass a theme to the style prop on StreamVideo. The theme file lists every property you can override.

App.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, {useMemo, useState} from 'react'; ... export default function App() { ... // Avoid passing inline styles to the component, as it will cause unnecessary re-renders const theme = useMemo( () => ({ callControlsButton: { container: { borderRadius: 10, }, }, hangupCallButton: { container: { backgroundColor: 'blue', }, }, toggleAudioPublishingButton: { container: { backgroundColor: 'green', }, }, }), [], ); ... return ( <StreamVideo client={client} style={theme}> ... </StreamVideo> ); ... }

Preview of the video view

Checkpoint: the control bar sits in your orange container, the top bar shows participant names and the dominant speaker, and the buttons use your theme colors.

Verify the whole build

Build to your device and run the full loop: tap "Join Video Call", allow camera and microphone, join from a browser tab, and check both video tiles render. Toggle your mic and watch the indicator update in the browser. Hang up and confirm you land back on the home screen.

Customize further

The UI components reference lists everything the SDK ships with. Common next customizations:

Troubleshooting

  • useConnectedUser() stays undefined - the client never connected. Check the console for an auth error, which usually means the API key and token belong to different apps.
  • token is invalid / auth error - the token was minted for a different app, or it expired. Re-mint with getstream token <user-id> and confirm the API key matches.
  • project credentials missing - the CLI has not been initialized here. Run getstream init in the project directory first.
  • No video, only a black tile - you're on an emulator. Emulators have no camera; build to a physical device.
  • iOS build fails to find a pod - run bundle install then bundle exec pod install from the ios folder, or npx pod-install from the project root.
  • Android build fails on minSdkVersion - confirm minSdkVersion is at least 24 in the buildscript.ext block of android/build.gradle (RN 0.86 templates already set it).
  • iOS device build fails to sign - open the project in Xcode, pick a Team under Signing & Capabilities, and change the bundle identifier if the default is taken.
  • Camera and mic permission prompts never appear - the usage description keys are missing from Info.plist, or the permissions are missing from AndroidManifest.xml (Step 2).
  • Participant count stays at 1 - two sessions signed in as the same user count as one participant. Use a different user id in the second session.
  • Stuck on "Joining call..." - call.join() rejected. The .catch() in the useEffect logs the reason to the console.

Recap

Please let us know if you encounter issues building a video calling app with our React Native SDK. Our team is also happy to review your UI designs and advise you on how to integrate them with Stream.

To recap what we've learned about Stream's video calling service:

  • You set up a call: (const call = client.call("default", "your-call-id")).
  • The call type ("default" in the above case) controls which features are enabled and how permissions are set.
  • When you join a call, real-time communication is set up for audio & video calling: (call.join()).
  • State-related hooks such as useParticipantCount and useDominantSpeaker make it easy to build your UIs.
  • The CallContent component renders audio and video and adds buttons to control streaming options.

Using the Stream's Video Calling API, all calls run on a global edge network of video servers closer to all users. Being closer to your users improves call latency and reliability. The React Native SDK enables you to build in-app video calling, audio rooms, and live streaming in days.

We hope you've enjoyed this tutorial. Please feel free to reach out if you have any suggestions or questions.

Next steps

Pricing is flexible with multiple tiers, including a free maker plan for hobby projects and small companies.

Final Thoughts

In this video calling app tutorial we built a fully functioning React Native video app with our React Native SDK component library. We also showed how easy it is to customize the behavior and the style of the React Native video app components with minimal code changes.

Both the video SDK for React Native and the API have plenty more features available to support more advanced use cases.

Machine-readable resources

For AI agents and coding assistants working with this SDK:

  • CLI + skills: curl -fsSL https://getstream.io/cli.sh | bash, then getstream skills and getstream skills stream-react-native for the React Native pack. (Alternative: npx skills add GetStream/agent-skills -s stream.)
  • Provisioning: getstream init (auth + create/select org & app) -> getstream token <user-id> (mint a token). getstream env --target <expo|ios|android> writes keys into a project file where the platform has a convention for it.
  • Data & config from the CLI: getstream api <Endpoint> --request '{...}' for users, calls, and call members
  • React Native integration skill: invoke /stream-react-native in your agent for React Native and Expo setup patterns; /stream-docs searches live SDK docs with citations
  • Docs index for LLMs: https://getstream.io/video/docs/react-native/llms.txt
  • Markdown endpoints: append .md to any docs URL for a clean, token-efficient version
  • Source of truth for APIs: the SDK repository - check the installed version's source rather than assuming APIs from training data

Give us feedback!

Did you find this tutorial helpful in getting you up and running with your project? Either good or bad, we're looking for your honest feedback so we can improve.

Start coding for free

No credit card required.
If you're interested in a custom plan or have any questions, please contact us.