Skip to main content
🤖 LLM Friendly: This page is available in raw Markdown format for LLM consumption:voip-push.md|Get full documentation:llms.txt/llms-full.txt

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:

  1. Looks up the device's api_key_sid and platform (ios/android).
  2. Queries your Push Credential (configured in Workspace Settings → Push Credentials) that matches the SID and platform.
  3. Decrypts your stored APNs/FCM credentials.
  4. 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:

ItemDescription
Key ID10-character identifier of your APNs auth key
Team ID10-character Apple Developer Team ID
Private Key (.p8)Downloaded .p8 file content
Bundle IDYour app's bundle identifier (e.g., com.yourcompany.app)
Environmentsandbox 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"
}'
caution

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_environment matches the build type.

Android (Firebase Cloud Messaging)​

You'll need from the Firebase Console:

ItemDescription
Service Account JSONFirebase Admin SDK service account key (download from Project Settings → Service Accounts)
Package NameYour 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"
}
important

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"
}'
ParameterTypeRequiredDescription
push_tokenstringYesVoIP push token (PushKit on iOS, FCM token on Android)
device_idstringYesUnique device identifier (e.g., UUID)
platformstringYesios or android
os_versionstringNoDevice OS version
app_versionstringNoYour app version
device_modelstringNoDevice model name
tip

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.

info

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-token is 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"
}
info

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:

  1. User taps Answer → Your app opens a WebSocket to ws_url.
  2. Authenticate → Send session.connect with the call_token within 3 seconds:
    {
    "event": "session.connect",
    "data": { "token": "YOUR_CALL_TOKEN" }
    }
  3. Receive SDP Offer → The server sends call.offer with the WebRTC SDP.
  4. Send SDP Answer → Your app creates and sends call.answer with the local SDP.
  5. 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_token from the VoIP push payload directly as the Bearer token (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 Here to 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:

  1. 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.
  2. Autonomous Offline Fallback (ring_timeout_secs): If the mobile device loses network connectivity or enters airplane mode while ringing, the local countdown timer started from ring_timeout_secs ensures that the incoming call UI dismisses automatically once the timer expires.

iOS Entitlements​

Ensure your iOS project has:

  • Background Modes → Voice over IP enabled (UIBackgroundModes containing voip)
  • 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:

  1. Generate a new key in the Apple Developer Portal / Firebase Console.
  2. Update the push credential via PATCH /push-credentials/:id with the new key.
  3. The change takes effect immediately — no app update needed.

For the full CRUD API reference, see Push Credentials API.