Push Notifications
The server side of waking a mobile app for an incoming call: enabling the wake-up hold, the call.mobile_push_wakeup webhook, and sending the push from your backend.
Everything that happens in the app (the softphone, CallKit and Telecom, and handling the push when it arrives) is covered in the Mobile Integration Guide.
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.
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
When a call arrives for a user with mobile_push_wakeup on and no active session, DialStack holds the call and sends your backend a call.mobile_push_wakeup webhook. Your backend sends a push to the user's devices. The app wakes, reconnects, and receives the held call. See The Wake Flow for the app's side.
You have approximately 30 seconds from when the call arrives until it times out. That covers push delivery, the app waking and reconnecting, and the user answering.
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
The examples below show the transport settings. The exact payload shape depends on the OS call library in your app, which reads the push; whatever the shape, include the webhook's call_id, so the app can pair the push with the call it receives.
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. Verify the signature over the raw request body (re-serializing parsed JSON changes the bytes and the signature won't match), then look up the user's devices and send the pushes:
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// See Signature Verification in the webhook docs for the algorithm.
function verifySignature(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries((header ?? '').split(',').map((p) => p.split('=')));
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return (
expected.length === v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
);
}
app.post('/webhooks/dialstack', express.raw({ type: 'application/json' }), async (req, res) => {
const rawBody = req.body.toString('utf8');
if (
!verifySignature(
rawBody,
req.headers['x-dialstack-signature'],
process.env.DIALSTACK_WEBHOOK_SECRET
)
) {
return res.status(401).send('Invalid signature');
}
const { type, data } = JSON.parse(rawBody);
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');
});
The secret is the one returned when you created the webhook endpoint. See Signature Verification for the header format.
Best Practices
- 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