---
sidebar_position: 2
title: VoIP Push Notifications
description: Complete integration guide for receiving incoming call VoIP push notifications on custom partner mobile apps using the Client API and Firetell Push Credentials.
---

# 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.

```mermaid
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](/docs/rest-api/workspace-api/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](https://console.firetell.com) or the [API Keys endpoint](/docs/rest-api/workspace-api/push-credentials). Note the **SID** (`sid-xxx`) and **Secret Key**.

### Step 2: Configure Push Credentials

Upload your APNs or FCM credentials via the [Push Credentials API](/docs/rest-api/workspace-api/push-credentials) or via **Console → Settings → Push Credentials**.

#### iOS (APNs Token-Based / JWT)

You'll need from the [Apple Developer Portal](https://developer.apple.com/account/resources/authkeys/):

| 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 |

```bash
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](https://console.firebase.google.com/):

| 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)                                 |

```bash
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](/docs/rest-api/client-api/overview) for the full JWT specification.

```json
{
  "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:

```bash
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                                      |

:::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](/docs/rest-api/agent-api/voip-push-incoming-call#push-payload-specification), 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`).

```json
{
  "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)

```json
{
  "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:**
```json
{
  "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:**
```json
{
  "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](/docs/rest-api/agent-api/voip-push-incoming-call) 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**:
   ```json
   {
     "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](/docs/rest-api/agent-api/calls#native-websocket-signaling-protocol).

### 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:

```http
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](/docs/rest-api/workspace-api/push-credentials).
