Abrova
☰ Docs menu · VoIP calls
Coming soon. Abrova Push is coming soon. This page describes how it will work; it can’t be used yet.

VoIP Add-on

Native incoming call UI with CallKit (iOS) and CallStyle (Android) for voice and video calling apps. No FCM or Google Play Services required.

Flutter 0.1.1React Native 0.1.1Android 0.1.0iOS 0.1.2iOS 13+Android 5.0+Last updated: 27 Oct 2026

Native incoming call UI with CallKit (iOS) and CallStyle (Android) for voice and video calling apps.

No FCM or Google Play Services Required.

Works on all devices including Huawei and non-Google Android phones.

Flutter#

The VoIP add-on is a separate package, abrova_push_voip, used alongside abrova_push:

Both install from Abrova Center:

yaml
dependencies:
  abrova_push:
    hosted: https://center.abrova.ir/api/packages/abrova/pub
    version: ^0.1.20
  abrova_push_voip:
    hosted: https://center.abrova.ir/api/packages/abrova/pub
    version: ^0.1.1

The Android side comes from the Abrova Center Maven repository you added for abrova_push (see Flutter SDK). On iOS, add the native VoIP SDK inside the app target in ios/Podfile:

ruby
pod 'AbrovaPushVoip', :git => 'https://github.com/Abrova-co/abrova-push-voip-ios-sdk.git', :tag => '0.1.2'

iOS Setup

Add to your Info.plist:

xml
<key>UIBackgroundModes</key>
<array>
  <string>voip</string>
  <string>remote-notification</string>
</array>

Enable Push Notifications and Background Modes → Voice over IP, Remote notifications in Xcode under Signing & Capabilities. No AppDelegate changes are needed: the plugin handles PushKit registration and CallKit.

Android Setup

No manifest changes are needed. The call activities, receivers and permissions (USE_FULL_SCREEN_INTENT, VIBRATE, FOREGROUND_SERVICE) are declared by the native ir.abrova:abrova-push-voip library and merged automatically.

Initialize VoIP (Flutter)

dart
import 'package:abrova_push/abrova_push.dart';
import 'package:abrova_push_voip/abrova_push_voip.dart';

// Initialize AbrovaPush first
await AbrovaPush.init(AbrovaPushConfig(apiKey: 'ab_live_your_api_key'));
await AbrovaPush.register();

// Then initialize VoIP with its config and callbacks
await AbrovaPushVoIP.init(
  config: AbrovaPushVoIPConfig(
    appName: 'MyApp',
    callTimeout: 30,        // Seconds before an unanswered call times out (default: 60)
    ringtone: null,         // Custom ringtone (optional)
  ),
  onCallAccepted: (callData) {
    print('Call accepted: ${callData.callId}');
    print('Caller: ${callData.callerName}');
    print('Type: ${callData.mediaType}'); // CallMediaType.audio or .video

    // Navigate to your call screen
    Navigator.push(context, MaterialPageRoute(
      builder: (_) => CallScreen(call: callData),
    ));
  },
  onCallDeclined: (callData) {
    // Notify your server that call was declined
  },
  onCallTimeout: (callData) {
    // Show missed call notification
  },
);

// Let the plugin register its PushKit (VoIP) token with Abrova
final deviceId = await AbrovaPush.getDeviceId();
await AbrovaPushVoIP.setApiKey(apiKey: 'ab_live_your_api_key', deviceId: deviceId);

Handle Initial Call (Cold Start)

When the app is launched from an accepted call notification:

dart
// Check for initial call when app starts
final initialCall = await AbrovaPushVoIP.getInitialCall();
if (initialCall != null) {
  // App was opened from accepting a call
  Navigator.push(context, MaterialPageRoute(
    builder: (_) => CallScreen(call: initialCall),
  ));
}

End Call

dart
await AbrovaPushVoIP.endCall(callData.callId);

// Report call as connected (for CallKit on iOS)
await AbrovaPushVoIP.reportCallConnected(callData.callId);

Call Data Model

dart
class CallData {
  final String callId;               // Unique call identifier
  final String callerName;           // Display name
  final String? callerAvatar;        // Avatar URL
  final CallMediaType mediaType;     // CallMediaType.audio or CallMediaType.video
  final Map<String, dynamic> extra;  // Extra data
  final DateTime receivedAt;         // When call was received
}

React Native#

The VoIP add-on is a separate package, used alongside abrova-push-react-native:

Install it from Abrova Center, like the core package:

bash
npm install abrova-push-react-native-voip --registry=https://center.abrova.ir/api/packages/abrova/npm/

On iOS, add the native VoIP SDK inside the app target in ios/Podfile, then run pod install:

ruby
pod 'AbrovaPushVoip', :git => 'https://github.com/Abrova-co/abrova-push-voip-ios-sdk.git', :tag => '0.1.2'

iOS Setup

Add VoIP background mode to Info.plist:

xml
<key>UIBackgroundModes</key>
<array>
  <string>voip</string>
  <string>remote-notification</string>
</array>

Initialize VoIP (React Native)

typescript
import AbrovaPush from 'abrova-push-react-native';
import AbrovaPushVoip, { CallData } from 'abrova-push-react-native-voip';

await AbrovaPushVoip.init({
  appName: 'MyApp',
  callTimeout: 30,
});

// Let the plugin register its PushKit (VoIP) token with Abrova (iOS)
const deviceId = await AbrovaPush.getDeviceId();
await AbrovaPushVoip.setApiKey({ apiKey: 'ab_live_your_api_key', deviceId: deviceId ?? undefined });

const unsubAccepted = AbrovaPushVoip.onCallAccepted((call: CallData) => {
  navigation.navigate('CallScreen', { call });
});

const unsubDeclined = AbrovaPushVoip.onCallDeclined((call: CallData) => {
  // Handle decline
});

const unsubTimeout = AbrovaPushVoip.onCallTimeout((call: CallData) => {
  // Handle timeout
});

// Clean up
useEffect(() => {
  return () => {
    unsubAccepted();
    unsubDeclined();
    unsubTimeout();
  };
}, []);

Handle Initial Call

typescript
const initialCall = await AbrovaPushVoip.getInitialCall();
if (initialCall) {
  navigation.navigate('CallScreen', { call: initialCall });
}

Call Control

typescript
await AbrovaPushVoip.endCall(call.callId);
await AbrovaPushVoip.reportCallConnected(call.callId);

TypeScript Types

typescript
interface CallData {
  callId: string;
  callerName: string;
  callerId?: string;
  callerAvatar?: string;
  callType: 'audio' | 'video';
  payload?: Record<string, any>;
  timestamp: number;
}

interface VoipConfig {
  appName: string;
  ringtoneUri?: string;
  vibrate?: boolean;          // default: true
  callTimeout?: number;       // seconds, default: 30
  callIdKey?: string;         // payload key overrides
  callerNameKey?: string;
  callerIdKey?: string;
  callerAvatarKey?: string;
  callTypeKey?: string;
}

Android#

Add the VoIP module to your build.gradle. It comes from the Abrova Center repository (https://center.abrova.ir/api/packages/abrova/maven) you added for the Android SDK:

kotlin
dependencies {
    implementation("ir.abrova:abrova-push-voip:0.1.0")
}

Manifest Configuration

No manifest changes are needed. The library's manifest already declares the permissions (VIBRATE, USE_FULL_SCREEN_INTENT, FOREGROUND_SERVICE), IncomingCallActivity, the Android 12+ trampoline activities (CallTrampolineActivity, DeclineCallActivity) and the receivers, and they are merged into your app automatically.

Initialize VoIP (Android)

kotlin
import ir.abrova.push.voip.AbrovaPushVoip
import ir.abrova.push.voip.VoipCallback
import ir.abrova.push.voip.VoipConfig
import ir.abrova.push.voip.CallData

// Initialize in Application.onCreate()
AbrovaPushVoip.initialize(
    context = this,
    config = VoipConfig(
        appName = "MyApp",
        timeoutSeconds = 30,
        showMissedCallNotification = true
    )
)

// Set callback
AbrovaPushVoip.setCallback(object : VoipCallback {
    override fun onCallAccepted(callData: CallData) {
        Log.d("VoIP", "Call accepted: ${callData.callId}")
        // Start your call activity
        startCallActivity(callData)
    }

    override fun onCallDeclined(callData: CallData) {
        Log.d("VoIP", "Call declined: ${callData.callId}")
    }

    override fun onCallTimeout(callData: CallData) {
        Log.d("VoIP", "Call timeout: ${callData.callId}")
    }

    override fun onError(error: String) {
        Log.e("VoIP", "Error: $error")
    }
})

Handle Incoming Call from Push (Android)

Incoming voip_call messages are picked up automatically: the library registers a receiver for Abrova Push messages that shows the call UI even when your app is in the background. If you also handle messages yourself, you can hand them to the VoIP module:

kotlin
AbrovaPush.setCallback(object : AbrovaPushCallbackAdapter() {
    override fun onMessageReceived(message: AbrovaPushMessage) {
        val data = message.data ?: return
        if (AbrovaPushVoip.handlePushMessage(data)) return
        // Handle other messages
    }
})

Manual Call Trigger (Android)

kotlin
val callData = CallData(
    callId = "unique-call-id",
    callerName = "John Doe",
    callerId = "user-123",
    callerAvatar = "https://example.com/avatar.jpg",
    callType = "video"
)

AbrovaPushVoip.showIncomingCall(callData)

Call Control (Android)

kotlin
// End call
AbrovaPushVoip.endCall(callId)

// Report call as connected
AbrovaPushVoip.reportCallConnected(callId)

// Get initial call (if app was cold started from call)
val initialCall = AbrovaPushVoip.getInitialCall()
initialCall?.let { startCallActivity(it) }

iOS#

Add AbrovaPushVoip with Swift Package Manager. In Xcode: File → Add Package Dependencies → enter the repository URL, choose version 0.1.2 or later and add the AbrovaPushVoip library to your app target:

https://github.com/Abrova-co/abrova-push-voip-ios-sdk.git

Capabilities

In Xcode, add these capabilities under Signing & Capabilities:

  • Background Modes: Voice over IP, Remote notifications
  • Push Notifications

Info.plist

xml
<key>UIBackgroundModes</key>
<array>
  <string>voip</string>
  <string>remote-notification</string>
</array>

Initialize VoIP (iOS)

swift
import AbrovaPushVoip

// In AppDelegate.didFinishLaunchingWithOptions
let config = VoipConfig(
    appName: "MyApp",
    callTimeout: 30,
    supportsVideo: true,
    ringtoneName: "ringtone.caf",
    iconTemplateName: "CallIcon"
)

AbrovaPushVoip.shared.initialize(config: config)
AbrovaPushVoip.shared.delegate = self
AbrovaPushVoip.shared.registerForVoIPPushes()

Implement Delegate (iOS)

swift
extension AppDelegate: AbrovaPushVoipDelegate {
    func voip(_ voip: AbrovaPushVoip, didAcceptCall callData: VoipCallData) {
        print("Call accepted: \(callData.callId)")
        // Navigate to call screen
        DispatchQueue.main.async {
            self.showCallScreen(callData)
        }
    }

    func voip(_ voip: AbrovaPushVoip, didDeclineCall callData: VoipCallData) {
        print("Call declined: \(callData.callId)")
        // Notify server
    }

    func voip(_ voip: AbrovaPushVoip, didTimeoutCall callData: VoipCallData) {
        print("Call timeout: \(callData.callId)")
        // Show missed call
    }

    func voip(_ voip: AbrovaPushVoip, didFailWithError error: String) {
        print("VoIP error: \(error)")
    }
}

Handle Initial Call (iOS)

swift
if let initialCall = AbrovaPushVoip.shared.getInitialCall() {
    showCallScreen(initialCall)
    AbrovaPushVoip.shared.clearInitialCall()
}

Call Control (iOS)

swift
AbrovaPushVoip.shared.endCall(callId: callData.callId)
AbrovaPushVoip.shared.reportCallConnected(callId: callData.callId)

Get VoIP Token (iOS)

swift
// Read the current PushKit (VoIP) token
if let token = AbrovaPushVoip.shared.getVoIPToken() {
    print("VoIP token: \(token)")
}

The native AbrovaPushVoip SDK does not upload this token to Abrova (the Flutter and React Native plugins do, through setApiKey). Abrova sends VoIP pushes to the VoIP token stored for the device, which the core AbrovaPush SDK uploads when it is initialized with usePushKit: true.

Web#

Web Limitation.

VoIP calling features are only available for native mobile platforms (iOS, Android, Flutter, React Native). Web applications should use WebRTC directly for voice/video calls.

Server-Side: Send VoIP Call#

Node.js SDK

typescript
import { AbrovaPush } from '@abrova/push-node';

const abrovaPush = new AbrovaPush({
  apiKey: 'ab_live_your_api_key',
  serverSecret: 'ab_srv_your_server_secret',
});

await abrovaPush.push.sendToDevice({
  deviceId: 'DEVICE_ID',
  title: 'Incoming call',
  body: 'John Doe is calling',
  data: {
    type: 'voip_call',
    callId: 'unique-call-123',
    callerName: 'John Doe',
    callerId: 'user-456',
    callerAvatar: 'https://example.com/avatar.jpg',
    callType: 'video',
  },
});

REST API

bash
curl -X POST https://push-api.abrova.ir/push/send \
  -H "x-api-key: ab_live_your_api_key" \
  -H "x-server-secret: ab_srv_your_server_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "deviceId": "DEVICE_ID",
    "title": "Incoming call",
    "body": "John Doe is calling",
    "data": {
      "type": "voip_call",
      "callId": "unique-call-123",
      "callerName": "John Doe",
      "callerId": "user-456",
      "callerAvatar": "https://example.com/avatar.jpg",
      "callType": "video"
    }
  }'

Payload Fields#

OptionTypeRequiredDescription
typestringYesMust be "voip_call" (system trigger)
callIdstringNoUnique call identifier; a random ID is generated if missing
callerNamestringNoDisplay name of caller (falls back to "Unknown")
callerIdstringNoUser ID of caller
callerAvatarstringNoAvatar URL (shown in notification and call screen)
callTypestringNo"audio" or "video" (default: audio)
Fixed System Trigger.

The type: "voip_call" field is the system trigger — it tells the SDK to show the incoming call UI. This value is fixed and cannot be changed. The other fields (callerName, callerId, etc.) are configurable via VoipConfig payload key mappings. The request itself also needs title and body (or a templateId) like any push.

How It Works#

  1. Server sends VoIP notification — Your server sends a push with type: "voip_call" to the device
  2. Native call UI appears — iOS shows CallKit, Android shows CallStyle notification (Android 12+) or full-screen activity
  3. User accepts or declines — Your callback is triggered with the call data
  4. Navigate to your call screen — Use WebRTC or your preferred calling solution for the actual call
Was this page helpful?
Report a problem