Mobile & Push Notifications
Build mobile softphone apps with push notifications so incoming calls can wake a backgrounded app.
Mobile clients still need to register an emergency address for the user — see Emergency Calling (E911). Nomadic use cases make this especially important on mobile, since users routinely change networks and locations.
Push Notifications
When a mobile app is in the background or closed, the WebRTC signalling connection is not active. Push notifications bridge this gap — DialStack notifies your backend via webhook, and your backend sends a push to wake the app.
Enabling Wake-Up Hold
By default, when a user has no active calling session, an incoming call moves on to their other devices and voicemail immediately. For DialStack to instead hold the call while your push wakes the app, enable mobile_push_wakeup on the user:
await dialstack.users.update(
userId,
{ config: { mobile_push_wakeup: true } },
{ dialstackAccount: accountId }
);
Enable this for users your application can actually reach with push notifications (for example, when they register a device token). With the flag on, the call keeps ringing toward the user's app for a wake-up window — long enough for push delivery, app wake-up, and connection — before falling through to the user's normal routing (next Find Me / Follow Me step or voicemail). With the flag off, there is no wait.
How It Works
┌──────────┐ ┌──────────────┐ ┌───────────┐ ┌──────────┐
│ Caller │ │ DialStack │ │ Your │ │ User's │
│ │ │ │ │ Backend │ │ Phone │
└────┬─────┘ └──────┬───────┘ └─────┬─────┘ └────┬─────┘
│ Calls user │ │ │
│────────────────>│ │ │
│ │ │ │
│ │ 1. Webhook: │ │
│ │ call.mobile_ │ │
│ │ push_wakeup │ │
│ │─────────────────>│ │
│ │ │ │
│ │ │ 2. Push │
│ │ │ notification │
│ │ │──────────────>│
│ │ │ │
│ │ │ 3. App wakes│
│ │ │ connects WS │
│ │ <───────────────────────────────│
│ │ │ │
│ │ 4. call.incoming│ │
│ │ (via WebSocket) │ │
│ │─────────────────────────────────>│
│ │ │ │
│ │ │ 5. User │
│ <─────────────────────────────────── answers │
│ Call connected │ │
- DialStack sends a
call.mobile_push_wakeupwebhook to your platform's webhook URL when the call is being delivered to the user's app - Your backend sends a push notification (APNs or FCM) to the user's device
- The app wakes and connects to the WebRTC signalling channel
- DialStack delivers the
call.incomingmessage over WebSocket - The user answers and the call connects
You have approximately 30 seconds from when the call arrives until it times out. Push notification delivery and app wake typically take 2–5 seconds, leaving plenty of time for the user to answer.
Webhook Payload
DialStack delivers a call.mobile_push_wakeup webhook event to your platform's webhook URL whenever an incoming call is being delivered to the app session of a user with mobile_push_wakeup enabled — whether the call reached them directly, through a ring group, or as a call queue agent. The user_id field identifies the user whose app should be woken, so you can look up their device tokens in your own database and send the push:
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v3",
"type": "call.mobile_push_wakeup",
"created_at": "2026-04-10T14:30:00Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"from_number": "+14155551234",
"from_name": "John Smith",
"to_number": "+14155559876",
"ringing_at": "2026-04-10T14:30:00Z"
}
}
call.incoming or call.ringing?call.incoming fires once when the call enters the platform, and its user_id is only present when the number routes directly to a single user. call.ringing fires per user being reached, but also when their calls forward to an external number — a push sent on it could wake the app for a call that never arrives, which iOS penalizes (every VoIP push must report a call). call.mobile_push_wakeup fires only when the user's app session is actually being rung, so every push corresponds to a real, answerable call.
Your backend maintains the mapping between DialStack user_id and your push notification device tokens. DialStack does not store device tokens — push delivery is entirely in your control.
Sending Push Notifications
iOS (APNs)
Send a VoIP push notification using the PushKit framework. VoIP pushes wake the app immediately and have higher priority than standard notifications.
import apn from 'apn';
const provider = new apn.Provider({
token: {
key: './AuthKey.p8',
keyId: 'YOUR_KEY_ID',
teamId: 'YOUR_TEAM_ID',
},
production: true,
});
async function sendCallPush(deviceToken, callData) {
const notification = new apn.Notification();
notification.topic = 'com.example.myapp.voip';
notification.pushType = 'voip';
notification.priority = 10;
notification.expiry = Math.floor(Date.now() / 1000) + 30; // 30 second TTL
notification.payload = {
call_id: callData.call_id,
from_number: callData.from_number,
from_name: callData.from_name,
to_number: callData.to_number,
};
await provider.send(notification, deviceToken);
}
Android (FCM)
Send a high-priority data message to bypass Doze mode:
import admin from 'firebase-admin';
admin.initializeApp({
credential: admin.credential.applicationDefault(),
});
async function sendCallPush(deviceToken, callData) {
await admin.messaging().send({
token: deviceToken,
android: {
priority: 'high',
ttl: 30000, // 30 seconds
},
data: {
type: 'call.mobile_push_wakeup',
call_id: callData.call_id,
from_number: callData.from_number,
from_name: callData.from_name || '',
to_number: callData.to_number,
},
});
}
Use FCM data messages (not notification messages) for incoming calls. Data messages wake the app immediately and let you show a full-screen call UI. Notification messages display a banner that the user must tap.
Webhook Handler
Your platform's webhook handler receives call.mobile_push_wakeup events for all accounts. Look up the user's devices and send notifications:
import express from 'express';
import { DialStack } from '@dialstack/sdk/server';
const app = express();
const dialstack = new DialStack(process.env.DIALSTACK_API_KEY);
// This is your platform-level webhook URL (configured during onboarding)
app.post('/webhooks/dialstack', express.json(), async (req, res) => {
// Verify the webhook signature
const isValid = dialstack.webhooks.verifySignature(
req.body,
req.headers['x-dialstack-signature'],
process.env.DIALSTACK_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const { type, data } = req.body;
if (type === 'call.mobile_push_wakeup' && data.user_id) {
// Look up the user's device tokens in YOUR database
const devices = await getDeviceTokensForUser(data.user_id);
// Send push to all registered devices in parallel
await Promise.all(
devices.map((device) => {
if (device.platform === 'apns') {
return sendApnsPush(device.token, data);
} else if (device.platform === 'fcm') {
return sendFcmPush(device.token, data);
}
})
);
}
res.status(200).send('OK');
});
Mobile App Flow
iOS (Swift)
// 1. Register for VoIP push notifications
import PushKit
class AppDelegate: UIResponder, PKPushRegistryDelegate {
func registerForVoIPPush() {
let registry = PKPushRegistry(queue: .main)
registry.delegate = self
registry.desiredPushTypes = [.voIP]
}
func pushRegistry(_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType) {
let token = pushCredentials.token.map {
String(format: "%02x", $0)
}.joined()
// Store the device token in YOUR backend, mapped to the user's
// DialStack user_id. Your backend uses this to send pushes
// when it receives a call.mobile_push_wakeup webhook.
YourAPI.shared.registerPushToken(token: token, platform: "apns")
}
// 2. Handle incoming VoIP push
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType) {
let callData = payload.dictionaryPayload
// 3. Report to CallKit (required on iOS)
let update = CXCallUpdate()
update.remoteHandle = CXHandle(
type: .phoneNumber,
value: callData["from_number"] as? String ?? ""
)
update.localizedCallerName = callData["from_name"] as? String
let callUUID = UUID()
CXProvider.shared.reportNewIncomingCall(
with: callUUID,
update: update
) { error in
if error == nil {
// 4. Connect to WebRTC in the background
DialStackPhone.shared.connect()
}
}
}
}
Android (Kotlin)
// 1. Handle FCM data message
class CallFirebaseService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
val data = message.data
if (data["type"] == "call.mobile_push_wakeup") {
// 2. Show full-screen call notification
val intent = Intent(this, IncomingCallActivity::class.java).apply {
putExtra("call_id", data["call_id"])
putExtra("from_number", data["from_number"])
putExtra("from_name", data["from_name"])
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Use a full-screen intent for incoming call UI
val notification = NotificationCompat.Builder(this, "calls")
.setSmallIcon(R.drawable.ic_call)
.setContentTitle(data["from_name"] ?: data["from_number"])
.setContentText("Incoming call")
.setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setFullScreenIntent(pendingIntent, true)
.build()
val manager = getSystemService(NotificationManager::class.java)
manager.notify(CALL_NOTIFICATION_ID, notification)
}
}
}
Background Audio
On mobile platforms, configure your app to continue audio playback in the background:
- iOS: Enable the
audiobackground mode in your app's capabilities - Android: Use a foreground service with the
mediaPlaybacktype during active calls
Battery Optimization
The WebSocket connection consumes battery when maintained in the background. For mobile apps:
- Maintain the WebSocket only while the app is in the foreground
- Rely on push notifications for incoming calls when backgrounded
Best Practices
DO
- Send push notifications with a short TTL (30 seconds) — stale call notifications confuse users
- Use VoIP pushes on iOS (PushKit) for immediate delivery and CallKit integration
- Use FCM data messages (not notification messages) on Android for full app control
- Include the
call_idin the push payload so the app can match it to the WebSocket event
DON'T
- Don't rely solely on the WebSocket for mobile incoming calls — the connection drops when backgrounded
- Don't keep the WebSocket alive in the background on mobile — it drains battery