Mobile Integration Guide
You can add DialStack calling to a mobile app at four levels. Each level adds capability, and each moves more of the work onto you. This guide explains what you get at each level, what you build, and when to choose it. The mobile example apps show each level working.
| Level | In one line | Start from |
|---|---|---|
| 1. In-app softphone | Drop the React Native softphone into your app. Calls work while the app is open. | expo, bare |
| 2. Softphone + OS integration | Add CallKit (iOS) and Telecom (Android), plus push wake. Calls ring on a locked phone and wake a closed app. | os-integration |
| 3. Build on the core SDK | Build your own UI and your own OS integration on DialStackPhone and Call. | Client SDK Reference |
| 4. Build on the protocol | Skip the SDK: a fully native app (Swift, Kotlin) speaks the signalling protocol directly. | Signalling Protocol |
If your users only need "call this number from my phone", and the app never has to carry audio, you may not need any of these. The click-to-call example rings the user's own cell phone over the carrier network, with no WebRTC and no push.
- Your own app release. You ship the app under your own developer accounts: store builds, signing, and the permissions and entitlements your level needs. DialStack does not ship an app for you.
- A backend. Your app gets its user session token from your server, because minting one takes your secret key, which must never be on a device (see Get a user token). Level 2 adds a webhook endpoint and a push sender to that server.
Capabilities by Level
| Capability | Level 1: in-app | Level 2: OS integration | Level 3: core SDK | Level 4: protocol |
|---|---|---|---|---|
| Place and receive calls with the app in the foreground | ✅ | ✅ | ✅ | ✅ |
| Incoming call rings while the app is in the background or the phone is locked | ❌ | ✅ | If you build it | If you build it |
| Incoming call wakes an app the OS has killed | ❌ | ✅ (push wake) | If you build it | If you build it |
| Answer from the lock screen, notification, car or watch | ❌ | ✅ | If you build it | If you build it |
| Calls appear in the OS call history (Recents) | ❌ | ✅ | If you build it | If you build it |
| Call audio and routing owned by the OS | ❌ | ✅ (CallKit / Telecom) | If you build it | If you build it |
| Outbound call keeps running when the user leaves the app mid-call | ❌ | ✅ | If you build it | If you build it |
| Ready-made softphone UI | ✅ | ✅ | ❌ (your own UI) | ❌ (your own UI) |
Your Responsibilities by Level
| Responsibility | Level 1: in-app | Level 2: OS integration | Level 3: core SDK | Level 4: protocol |
|---|---|---|---|---|
| Mobile app release (store builds, signing) | Yes | Yes, plus the VoIP push and call capabilities | Yes | Yes |
| Backend that mints user session tokens | Yes | Yes, callable by the app when a push wakes it with no UI | Yes | Yes |
Webhook endpoint for call.mobile_push_wakeup | No | Yes | If you want push wake | If you want push wake |
| Push server (FCM for Android, APNs VoIP for iOS) | No | Yes | If you want push wake | If you want push wake |
| Push credentials and a device-token registry | No | Yes | If you want push wake | If you want push wake |
| Native code | Only the WebRTC library's setup | An OS call library, an Android wake service, native timers | Whatever you build | All of it, including WebRTC media |
| Choice of OS call library | None | Yours. The example uses expo-callkit-telecom, with a patch | Yours | Yours |
| UI | The SDK's, or your own on its state | Same as Level 1 | Yours | Yours |
How the SDK Is Layered
The mobile SDK is a stack of layers, and you can stop at whichever layer suits you:
┌─────────────────────────────────────────────────────────────┐
│ React Native UI: <Softphone>, <DialPad>, <IncomingCall>, │ @dialstack/sdk-native
│ <OngoingCall> │
├─────────────────────────────────────────────────────────────┤
│ Call state: <SoftphoneProvider>, useSoftphone() │ @dialstack/sdk-native
├─────────────────────────────────────────────────────────────┤
│ NativeCallBridge: keeps each OS call and SDK call in step │ @dialstack/sdk-native
├─────────────────────────────────────────────────────────────┤
│ OsCallAdapter: the port to CallKit / Telecom │ you write it, over your
│ │ chosen OS call library
├─────────────────────────────────────────────────────────────┤
│ Core: DialStackPhone, Call (signalling, media, │ @dialstack/sdk-webrtc
│ reconnection, token refresh) │ (included in sdk-native)
└─────────────────────────────────────────────────────────────┘
- The core has no UI and no platform code, so the same core runs in a browser and on React Native.
- The call state is shared with the web. The React Native provider has the same API as the web softphone. Only the rendering differs.
- The OS side is a port, not a dependency. There are several OS call libraries for React Native, and your app may already use one, so the SDK doesn't pick one for you. The bridge holds the logic; you write one adapter over the library you choose.
At Level 1 you use the UI (or only the call state) and the core. At Level 2 you add the bridge and write the adapter. At Level 3 you use the core and write the rest. At Level 4 you use none of the SDK.
Level 1: In-App Softphone
What you get: a complete softphone for React Native in @dialstack/sdk-native. It places and receives calls, rings, plays ringback, manages the call audio, and handles mute, hold, transfer, DTMF and the E911 prompt.
What you do: install it, pass a session token from your backend, and render it.
<SoftphoneProvider token={token} storage={storage} onTokenExpiring={fetchFreshToken}>
<Softphone />
</SoftphoneProvider>
The @dialstack/sdk-native README covers installation and permissions, and the expo and bare examples are complete apps at this level.
Your Own UI on the SDK's Call Logic
If you only want a different look, keep <SoftphoneProvider> and build your screens on useSoftphone() instead of rendering <Softphone>. The provider still tracks the calls, decides which one is active, ringing or held, and carries out the actions. You only draw the result. This works at Level 2 too.
| You need | From useSoftphone() |
|---|---|
| Connection state | connection |
| The calls, and which one matters | calls, activeCall, incomingCalls, heldCalls |
| Placing and answering | placeCall(destination), answerCall(call), switchToCall(call) |
| Actions on the active call | actions: hangup, toggleMute, toggleHold, sendDtmf, transfer(destination) |
| Actions on any other call | actions.callActionsFor(call): reject, hangup, toggleMute, toggleHold, sendDtmf |
| Attended transfer, three-way | startAttendedTransfer, completeAttendedTransfer, mergeCalls and related |
| Display, errors, E911 | duration, displayNumber(), lastError, emergency |
Use answerCall() and placeCall() rather than calling the phone directly: they handle call waiting and, at Level 2, the OS.
Where It Stops
Level 1 works only while the app is in the foreground. Once the OS suspends or kills the app, incoming calls don't ring on the phone (they still ring the user's other devices and follow their routing). Nothing appears on the lock screen or in the call history.
Level 1 is enough when users only call while they are using the app, for example a dispatch app that stays open during a shift. If users expect the app to behave like a phone, you need Level 2.
Why a Mobile App Needs OS Integration
On the web, the browser keeps a softphone tab running and connected. On a phone, the OS decides when your app runs, and it gives the lock screen, the ringer and the audio only to calls it knows about. Your app can't keep its own connection open in the background either, so the only way to reach it there is the OS's push channel (APNs on iOS, FCM on Android). That's why several everyday situations need the OS's help:
| Situation | App only (Level 1) | With OS integration (Level 2) |
|---|---|---|
| A call comes in while the app is in the background | The OS has suspended the app, so it can't ring | A push reaches the phone and the OS rings it |
| A call comes in after the app was killed | Nothing is running, so nothing rings | The push makes the OS ring the phone and start the app |
| The phone is locked | An app can't draw over the lock screen | The OS's full-screen incoming-call screen appears |
| The user answers without opening the app | Not possible | From the lock screen, a notification, a car, headphones or a watch |
| The user leaves the app during a call | The OS can suspend the app and drop the call | The OS keeps the app running for the whole call |
| A cellular call comes in | The OS takes the audio | The OS offers the user a choice, such as hold and answer |
| The user looks for the call afterwards | Only inside your app | In the phone's call history |
Level 2: Softphone + OS Integration
What you get:
- On the server: a wake-up hold and the
call.mobile_push_wakeupwebhook. DialStack holds the call while your push wakes the app, and delivers it when the app reconnects. Push Notifications is the server-side reference: enabling the hold, the webhook, and sending the push. - In
@dialstack/sdk-native:NativeCallBridge, which keeps each OS call and SDK call in step in both directions (answer, decline, end, mute, hold, DTMF), pairs a push-woken OS call with the call DialStack delivers, and cleans up a wake whose call never arrives. It also includes theOsCallAdapterinterface with a contract test suite, and the helpers for the Android wake task. - The
os-integrationexample: a worked integration on both iOS (PushKit and CallKit) and Android (FCM and Telecom), including a reference push server.
What you do: DialStack does not send pushes to your app and stores no device tokens. You send the push, and you wire the OS call library into your app.
os-integration shows one complete way to do this, for you to follow and adapt. It uses one OS call library, expo-callkit-telecom, with a patch to its native code. Its push server is a local test harness: device tokens in memory, optional webhook signatures, and a token endpoint with no authentication. Build your own versions of these.
The Wake Flow
Caller DialStack Your server Phone (OS) Your app
│ call │ │ │ │
│────────────>│ 1. hold the call │ │ │
│ │ 2. webhook ───────────>│ │ │
│ │ │ 3. push ───────────>│ 4. OS rings │
│ │ │ │ 5. starts the app ────>│
│ │ <────────────────────────────────────────────────── 6. app reconnects │
│ │ 7. call delivered to the app ─────────────────────────────────────────>│
│ <───────────────────────────────────────────────── 8. user answers, audio flows ────│
The push must carry the webhook's call_id: that's how the bridge pairs the ringing OS call with the call DialStack delivers, so the user sees one call. In the example, a release build goes from a killed app to an answered call in about 3 to 5 seconds. If the call never arrives (the caller hung up, or someone answered elsewhere), the bridge ends the OS call.
What You Build
| Piece | Why | In the example |
|---|---|---|
An OsCallAdapter | Connects the bridge to the OS call library you choose. Check it with the contract suite in @dialstack/sdk-native/testing. | src/os/expoCallKitTelecomAdapter.ts |
| The bridge setup | One phone and one bridge for the whole app, created with createPhone() and shared with <SoftphoneProvider>. | src/callBridge.ts, index.js |
| Native timers | Android pauses JavaScript timers in a backgrounded app, which would stop the bridge's wake deadlines. | modules/background-timers |
| An Android wake service | Starts your app's JavaScript when a push reports a call. iOS needs nothing extra. | plugins/withWakeHeadlessService.js |
| A push server | Receives the webhook, verifies its signature, and pushes to each of the user's devices (FCM on Android, APNs VoIP on iOS). | scripts/wake-webhook-server.mjs |
| A token endpoint | A woken app needs a session token with nobody signed in. It must authenticate the device. | The harness's /session (unauthenticated) |
| Push credentials | A Firebase project and an APNs key. | — |
Then enable mobile_push_wakeup on each user your app can reach with a push. The os-integration README covers testing a wake end to end.
Level 3: Build on the Core WebRTC SDK
What you get: the core, DialStackPhone and Call: the signalling connection with automatic reconnection, WebRTC media, call control (answer, hold, mute, DTMF, transfer), token refresh, presence and the emergency address API. See the Client SDK Reference.
On the web, the core is @dialstack/sdk-webrtc. On React Native, create the phone with createPhone() from @dialstack/sdk-native, which includes the core and applies the React Native settings it needs. Don't install @dialstack/sdk-webrtc alongside it.
What you build: the UI, the ringtone, the audio routing, and the app lifecycle. For the lifecycle, handle the app returning from the background, because the OS may have closed the connection while the app was away. Pass onAppResume to createPhone() so the phone checks its connection on return, or disconnect when the app goes to the background and reconnect when it returns. For lock-screen ringing and push wake, add everything from Level 2.
If you only want your own UI, you don't need this level: see Your own UI on the SDK's call logic. For a fully native app (Swift or Kotlin, no JavaScript), see Level 4.
Level 4: Build on the Signalling Protocol
For a fully native app (Swift or Kotlin) that won't adopt React Native, skip the SDK and speak the protocol directly.
What you get: the documented Signalling Protocol: authentication with a user session token, the WebSocket handshake, in-band token refresh, keepalive, the SDP and ICE exchange, call control and presence. The server side of push wake (the wake-up hold and call.mobile_push_wakeup) works exactly as at Level 2.
What you build: everything from Level 3, plus a WebSocket client (token refresh, keepalive, reconnection), WebRTC media on a native library such as libwebrtc, with the SDP and ICE exchange over the socket, and the call state machine. For lock-screen ringing and push wake, add your own CallKit and Telecom integration, and pair each push with its call by call_id.
You also own protocol versioning (the subprotocol your client negotiates), and every behaviour the SDK would otherwise handle for you.
Example Apps
All examples live in the public DialStack SDK repository under examples/mobile/.
| Example | Level | What it shows |
|---|---|---|
expo | 1 | <Softphone> in an Expo app |
bare | 1 | The same in a bare React Native app |
os-integration | 2 | Push wake, CallKit and Telecom over expo-callkit-telecom, and a reference push server |
click-to-call | — | Calling through the user's own cell phone, with no WebRTC |
Related Resources
- Push Notifications: the server side of push wake: the wake-up hold, the webhook, and sending the push
call.mobile_push_wakeup: the webhook reference- Softphone (web): the web softphone, which shares the React Native softphone's provider and hooks
- Client SDK Reference:
DialStackPhoneandCall - Emergency Calling (E911)