Streaming CDN

Native incoming calls in React Native: CallKit on iOS, full-screen calls on Android

Updated 2026-10-07

A calling app has to ring when it is not running: on a locked iPhone, on an Android phone in a pocket, after the system has reclaimed the process. That takes a push that wakes the app and a system incoming-call screen, on each platform in its own way. This guide shows how the Streaming CDN calling SDK does it for React Native and Expo.

How a call reaches a sleeping phone

  1. The caller starts a call; the call service sends a push with the call id, caller and mode (audio or video).
  2. iOS: a VoIP push (PushKit) wakes the app, which reports the call to CallKit. Android: an FCM message wakes the app, which shows a full-screen incoming-call notification.
  3. The person answers from the lock screen. The app accepts the call with its client token.
  4. The service makes the accept atomic — the first device to answer wins — and returns the media credential to that device only. Every other device signed in as the same person stops ringing.

The push itself carries no media token and no server key; the media credential is returned only after the accept succeeds.

{
  "callId": "rtc_call_...",
  "callerId": "user-17",
  "callerName": "Alex",
  "mode": "video",
  "expiresAt": "2026-07-26T04:00:00Z"
}

Install

npm install @streaming-cdn/rtc-react-native react-native-webrtc

Join a call

Your backend keeps the server API key and hands the app a short-lived credential:

import { ReactNativeRtcClient } from "@streaming-cdn/rtc-react-native";

const identity = await fetch("https://your-backend.example.com/api/rtc/identity", {
  credentials: "include"
}).then((response) => response.json());

const rtc = new ReactNativeRtcClient({
  credential: identity.credential,
  mode: "video",
  publishAudio: true,
  publishVideo: true,
  receiveAudio: true,
  receiveVideo: true
});
await rtc.join();

What the native call layer gives you

CapabilityAPIPlatform behaviour
Locked-screen and background callsRtcNativeIncomingCallsAndroid full-screen intent notification; iOS PushKit with CallKit
Ringtone and ringbackRtcCallTonesHonours the Android ringer mode; on iOS CallKit owns the incoming ringtone
Audio output routingclient.setAudioOutput(), RtcAudioRoutingEarpiece, speaker and Bluetooth through Android AudioManager and iOS AVAudioSession

On iOS a call identifier must be a UUID. On Android your app owns its Firebase registration and forwards payloads to the SDK, so Firebase stays an optional dependency.

The Android 14 trap: full-screen intent

USE_FULL_SCREEN_INTENT is what lets an incoming call take over the screen. Three facts to know before shipping:

  1. Since Android 14 (API 34) it is a special app access, granted automatically only to apps whose core function is alarms or calls. Other apps start without it.
  2. Since 31 May 2024, Google Play requires a declaration for it in the Play Console (App content → Sensitive permissions). Submit it before review.
  3. When the grant is missing there is no error: the call silently becomes an ordinary heads-up notification that is easy to miss. A build can pass testing and still be effectively uncallable on real devices.

Check at runtime and guide the user before the first call:

const caps = await RtcNativeIncomingCalls.getIncomingCallCapabilities();
// caps.presentation: "full-screen" | "notification" | "none"
if (caps.presentation !== "full-screen") {
  // explain why, then:
  await RtcNativeIncomingCalls.openFullScreenIntentSettings();
}

getIncomingCallCapabilities() also reports notificationsEnabled; when it is false the call will not appear at all. On iOS the result is always full-screen, because CallKit owns presentation.

Ringing ends on its own

A ringing invitation is valid for 45 seconds and a call nobody answers is closed as missed. On Android the incoming-call notification also carries a system timeout (45 seconds by default, ringTimeoutMs in the push payload, 10–120 seconds), so ringing stops even if the app process was reclaimed and no cancel can run.

Errors worth handling

ResponseMeaningWhat to do
401The client identity token expiredRefresh it through your backend
403This identity is not a recipient of the callDo not show the call
409Another device answered, or the call stopped ringingEnd the incoming UI quietly
Push after expiresAtThe call is overDiscard it without showing any UI

Real-time Communication SDK

Voice and video calling SDK for iOS, Android, Web, React Native and Expo, with CallKit and Android full-screen incoming calls, push wake-up and call states built in.

Learn more · Contact us