VoIP Push Notifications for Custom Apps
This guide explains how to set up VoIP push notifications for your own custom mobile application so that your end-users (Client API users) can receive incoming calls even when your app is in the background, suspended, or terminated.
sequenceDiagram
autonumber
actor Caller
participant Backend as Your Backend
participant Firetell as Firetell Platform
participant Push as APNs / FCM
actor User as Your Mobile App
Note over Backend: Generate Client JWT<br/>(aud: client-api, iss: sid-xxx)
Backend->>User: Deliver JWT to app
User->>Firetell: Register push token<br/>POST /me/devices/voip-push-token
Caller->>Firetell: Inbound Call
Note over Firetell: Resolve push credential<br/>by api_key_sid + platform
Firetell->>Push: Dispatch VoIP Push<br/>(using YOUR credentials)
Push->>User: Wake app with call.ring payload
User->>Firetell: Answer → WebSocket → WebRTC
How It Works​
When you integrate the Client API, your custom mobile app authenticates with a Client JWT signed using your API Key Secret. The iss claim in the JWT contains your API Key SID (sid-xxx).
When a client device registers a push token via POST /me/devices/voip-push-token, Firetell records the api_key_sid from the JWT alongside the device's push token.
When an incoming call arrives for that client, Firetell:
- Looks up the device's
api_key_sidandplatform(ios/android). - Queries your Push Credential (configured in Workspace Settings → Push Credentials) that matches the SID and platform.
- Decrypts your stored APNs/FCM credentials.
- Dispatches the VoIP push notification using your own APNs/FCM credentials.
This means incoming call pushes will arrive through your app's own bundle ID and push certificate — no dependency on the Firetell Agent app.
Setup Steps​
Step 1: Create an API Key​
Create an API Key in your workspace via Console → Settings → API Keys or the API Keys endpoint. Note the SID (sid-xxx) and Secret Key.
Step 2: Configure Push Credentials​
Upload your APNs or FCM credentials via the Push Credentials API or via Console → Settings → Push Credentials.
iOS (APNs Token-Based / JWT)​
You'll need from the Apple Developer Portal:
| Item | Description |
|---|---|
| Key ID | 10-character identifier of your APNs auth key |
| Team ID | 10-character Apple Developer Team ID |
| Private Key (.p8) | Downloaded .p8 file content |
| Bundle ID | Your app's bundle identifier (e.g., com.yourcompany.app) |
| Environment | sandbox for development, production for App Store / TestFlight |
curl -X POST "https://{workspace_id}.firetell.app/api/v1/push-credentials" \
-H "Authorization: ApiKey sk-YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My iOS App - Production",
"api_key_sid": "sid-650000000000000000000001",
"platform": "ios",
"apns_key_id": "ABC123DEFG",
"apns_team_id": "TEAM123456",
"apns_private_key": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMG...\n-----END PRIVATE KEY-----",
"apns_bundle_id": "com.yourcompany.app",
"apns_environment": "production"
}'
APNs Environment Mismatch
- Xcode debug builds on physical devices generate sandbox tokens.
- TestFlight and App Store builds generate production tokens.
- If users don't receive pushes, verify that
apns_environmentmatches the build type.
Android (Firebase Cloud Messaging)​
You'll need from the Firebase Console:
| Item | Description |
|---|---|
| Service Account JSON | Firebase Admin SDK service account key (download from Project Settings → Service Accounts) |
| Package Name | Your app's Android package name (optional but recommended) |
curl -X POST "https://{workspace_id}.firetell.app/api/v1/push-credentials" \
-H "Authorization: ApiKey sk-YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My Android App",
"api_key_sid": "sid-650000000000000000000001",
"platform": "android",
"fcm_service_account": "{\"type\": \"service_account\", \"project_id\": \"...\", ...}",
"fcm_package_name": "com.yourcompany.app"
}'
Step 3: Generate Client JWT on Your Backend​
Your backend must generate a signed JWT for each client user. See the Client API Overview for the full JWT specification.
{
"iss": "sid-650000000000000000000001",
"aud": "client-api",
"sub": "customer_12345",
"domain": "yourcompany.firetell.com",
"exp": 1786022400,
"call_flow_id": "cf_650000000000000000000100"
}
The iss claim must match the api_key_sid you configured in the Push Credential. This is how Firetell resolves which APNs/FCM credentials to use when dispatching the push.
Step 4: Register Push Token from Mobile App​
After the mobile app obtains its push token (PushKit on iOS, FCM on Android), register it with Firetell using the Client JWT:
curl -X POST "https://{workspace_id}.firetell.app/api/v1/me/devices/voip-push-token" \
-H "Authorization: Bearer YOUR_CLIENT_JWT" \
-H "Content-Type: application/json" \
-d '{
"push_token": "a1b2c3d4e5f6...device_token",
"device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"platform": "ios",
"os_version": "17.5.1",
"app_version": "1.0.0",
"device_model": "iPhone 15"
}'
| Parameter | Type | Required | Description |
|---|---|---|---|
push_token | string | Yes | VoIP push token (PushKit on iOS, FCM token on Android) |
device_id | string | Yes | Unique device identifier (e.g., UUID) |
platform | string | Yes | ios or android |
os_version | string | No | Device OS version |
app_version | string | No | Your app version |
device_model | string | No | Device model name |
Re-register the push token on every app launch or whenever the system issues a new token. PushKit and FCM may refresh tokens at any time.
Registering Regular Push Token for Cancellation (call.canceled & call.ended)
To allow your mobile app to automatically dismiss the ringing screen when a caller hangs up or the call times out, also register the device's regular push token via POST /me/devices/notification-push-token:
- On iOS: Register your standard APNs device token (
notification_push_provider: "apns") or Firebase FCM token (notification_push_provider: "fcm"). - On Android: The FCM token registered via
POST /me/devices/voip-push-tokenis automatically used for both incoming call pushes and cancellation pushes.
Push Payload​
When an incoming call arrives, Firetell dispatches a VoIP push with the following payload. The format is identical to the Agent API VoIP Push payload, but delivered using your APNs/FCM credentials.
iOS (APNs VoIP)​
The APNs topic is set to {your_bundle_id}.voip (e.g., com.yourcompany.app.voip).
{
"aps": {},
"event": "call.ring",
"workspace_id": "ws_123456789",
"call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
"call_token": "eyJhbGciOiJIUzI1NiIs...",
"ws_url": "wss://ws.firetell.app/ws",
"ring_timeout_secs": 30,
"caller_number": "+12025550143",
"caller_name": "Support Agent",
"caller_avatar": null,
"callee_number": "customer_12345",
"callee_name": "Customer Name",
"is_transfer": false,
"transfer_reason": null,
"is_video": false
}
Android (FCM Data Message)​
{
"event": "call.ring",
"workspace_id": "ws_123456789",
"call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
"call_token": "eyJhbGciOiJIUzI1NiIs...",
"ws_url": "wss://ws.firetell.app/ws",
"ring_timeout_secs": "30",
"caller_number": "+12025550143",
"caller_name": "Support Agent",
"caller_avatar": "",
"callee_number": "customer_12345",
"callee_name": "Customer Name",
"is_transfer": "false",
"transfer_reason": "",
"is_video": "false"
}
In FCM data messages, all values are strings. Parse ring_timeout_secs as an integer and is_transfer, is_video as booleans in your app.
Call Cancellation & End Payloads (call.canceled & call.ended)​
When an incoming call is canceled before answer (caller hangup, call flow timeout, or answered elsewhere), Firetell dispatches a background push notification so your app immediately dismisses the ringing screen:
iOS (APNs Silent Background Notification)​
- Topic:
{your_bundle_id}(without.voip) - Headers:
apns-push-type: background,apns-priority: 5 - Payload:
{
"aps": {
"content-available": 1
},
"event": "call.canceled",
"workspace_id": "ws_123456789",
"call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
"reason": "caller_hangup",
"timestamp": "2026-09-11T07:30:00.000Z"
}
Android & iOS via Firebase (FCM Data Message)​
- Priority:
high - TTL:
60s - Payload:
{
"event": "call.canceled",
"workspace_id": "ws_123456789",
"call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
"reason": "caller_hangup",
"timestamp": "2026-09-11T07:30:00.000Z"
}
Upon receiving call.canceled or call.ended, your mobile app should immediately call CXProvider.reportCall(with:endedAt:reason: .remoteEnded) on iOS or dismiss the full-screen intent / Telecom call on Android.
For complete payload field reference and iOS/Android implementation code samples (PushKit, CallKit, Firebase Messaging), see the VoIP Push Incoming Calls guide — the client-side handling is identical.
Handling the Incoming Call​
After your app receives the VoIP push and displays the native incoming call UI, the call answer flow is:
- User taps Answer → Your app opens a WebSocket to
ws_url. - Authenticate → Send
session.connectwith thecall_tokenwithin 3 seconds:{"event": "session.connect","data": { "token": "YOUR_CALL_TOKEN" }} - Receive SDP Offer → The server sends
call.offerwith the WebRTC SDP. - Send SDP Answer → Your app creates and sends
call.answerwith the local SDP. - Audio Connected → WebRTC media flows.
For the complete WebSocket signaling protocol and WebRTC setup, see Calls & WebSocket Signaling.
Declining / Rejecting the Call (HTTP Endpoint)​
If the user declines the incoming call on their device (e.g., lock-screen CallKit Decline or notification action), you do not need to open a WebSocket connection. Simply send an HTTP POST request:
POST https://{workspace_id}.firetell.app/api/v1/call-center/calls/{call_id}/reject
Authorization: Bearer <call_token>
Content-Type: application/json
{
"reason": "declined"
}
- Authentication: Pass the
call_tokenfrom the VoIP push payload directly as theBearertoken (or use your client JWT). - Speed & Reliability: Takes ~50ms to complete, reliably executing before the mobile OS suspends the background app.
- Signaling: The platform immediately sends SIP
486 Busy Hereto stop ringing the caller and forward or end the call.
Ring Timeout & Dismissal Architecture​
Firetell provides a multi-layer dismissal mechanism to prevent ghost ringing on mobile devices:
- Background Push Dismissal (
call.canceled/call.ended): When the caller hangs up while ringing, the Call Flow forwards the call, or another device answers, Firetell immediately dispatches a normal/background push notification (event: "call.canceled"or"call.ended"). Your app receives this in the background and dismisses the CallKit / Telecom UI immediately. - Autonomous Offline Fallback (
ring_timeout_secs): If the mobile device loses network connectivity or enters airplane mode while ringing, the local countdown timer started fromring_timeout_secsensures that the incoming call UI dismisses automatically once the timer expires.
iOS Entitlements​
Ensure your iOS project has:
- Background Modes →
Voice over IPenabled (UIBackgroundModescontainingvoip) - Push Notifications capability enabled
- VoIP Services enabled in your Apple Developer App ID configuration
Credential Rotation​
To rotate your APNs private key or FCM service account without downtime:
- Generate a new key in the Apple Developer Portal / Firebase Console.
- Update the push credential via
PATCH /push-credentials/:idwith the new key. - The change takes effect immediately — no app update needed.
For the full CRUD API reference, see Push Credentials API.