Integrating VoIP into Flutter AppsSIP, CallKit & Push Notifications
Build production-grade VoIP in Flutter — dart-sip-ua SIP registration, CallKit on iOS via APNs VoIP push, ConnectionService on Android via FCM, SRTP media encryption, and CGNAT traversal.
dart-sip-ua
SIP stack
500ms
wake-to-ring (iOS)
~0%
background battery drain
SRTP
media encryption
By Kaushik Parmar · Founder & VoIP Architect, CelloIP Technologies · December 21, 2025 · 19 min read
How do you add VoIP calling to a Flutter app?
Use dart-sip-ua for SIP signalling over WebSocket, flutter_callkit_incoming for native call UI, APNs VoIP push for iOS background wake, FCM high-priority messages for Android background wake, and flutter_webrtc with DTLS-SRTP for encrypted media.
Table of Contents
Why Mobile VoIP is Hard
Background Process Killing
iOS & Android kill background apps to save battery. A SIP registration cannot be maintained by a backgrounded app — you need push to wake it.
Native Call UI Required
Users expect the native calling screen (CallKit on iOS, ConnectionService on Android). A custom UI is rejected by app stores and ignored when locked.
Carrier-Grade NAT
CGNAT is ubiquitous on mobile networks. Standard SIP/UDP fails silently. WebSocket transport is the only reliable solution for mobile SIP.
Flutter VoIP Tech Stack
dart-sip-uaSIP stack (REGISTER, INVITE, BYE, DTMF)
flutter_callkit_incomingNative calling UI (iOS CallKit + Android)
flutter_webrtcWebRTC media — SRTP, DTLS, ICE
firebase_messagingFCM push for Android background calls
push_notification_iosAPNs VoIP push token registration
permission_handlerMicrophone, camera, notification permissions
SIP Registration with dart-sip-ua
dart-sip-ua provides a Dart-native SIP stack over WebSocket (wss://) — the only transport that works reliably through CGNAT on mobile networks. It handles REGISTER, INVITE, re-INVITE, BYE, and DTMF.
import 'package:dart_sip_ua/dart_sip_ua.dart';
class SipService implements SipUaHelperListener {
final _helper = SipUaHelper();
void init(String ext, String password, String serverWss) {
final settings = UaSettings()
..webSocketUrl = serverWss // wss://sip.celloip.com:8089/ws
..uri = 'sip:[email protected]'
..authorizationUser = ext
..password = password
..displayName = ext
..dtmfMode = DtmfMode.RFC2833;
_helper.addSipUaHelperListener(this);
_helper.start(settings);
}
@override
void registrationStateChanged(RegistrationState state) {
debugPrint('SIP status: ${state.state}');
// REGISTERED, UNREGISTERED, REGISTRATION_FAILED
}
@override
void callStateChanged(Call call, CallState state) {
if (state.state == CallStateEnum.CALL_INITIATION) {
// Show incoming call via flutter_callkit_incoming
showIncomingCallScreen(call);
}
}
void makeCall(String destination) {
_helper.call('sip:[email protected]', voiceonly: true);
}
}iOS: APNs VoIP Push + CallKit
iOS kills backgrounded apps to save battery, terminating the SIP WebSocket. Inbound calls arrive via APNs VoIP push — a special high-priority push channel. The app wakes, re-registers SIP, and shows the native CallKit ringing screen within 500ms.
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
Future<void> showIncomingCallScreen(String callerId, String uuid) async {
final params = CallKitParams(
id: uuid,
nameCaller: callerId,
appName: 'CelloIP',
type: 0, // 0 = voice, 1 = video
duration: 30000,
android: AndroidParams(
isCustomNotification: true,
isShowLogo: false,
ringtonePath: 'system_ringtone_default',
backgroundColor: '#1BA8DC',
),
ios: IOSParams(
iconName: 'CallKitLogo',
handleType: 'generic',
supportsVideo: false,
maximumCallGroups: 1,
maximumCallsPerCallGroup: 1,
ringtonePath: 'system_ringtone_default',
),
);
await FlutterCallkitIncoming.showCallkitIncoming(params);
}Android: FCM Push + ConnectionService
Android 14+ requirements: Declare FOREGROUND_SERVICE_PHONE_CALL permission and a proper telecom ConnectionService manifest declaration, or your app will be killed before it can show the call screen.
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
// Must be top-level function (not a class method)
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// Extract call info from FCM data message
final callerId = message.data['caller_id'] ?? 'Unknown';
final uuid = message.data['call_uuid'] ?? UniqueKey().toString();
// Show native call screen while app is backgrounded
await FlutterCallkitIncoming.showCallkitIncoming(
CallKitParams(
id: uuid,
nameCaller: callerId,
appName: 'CelloIP',
type: 0,
duration: 30000,
),
);
// Re-register SIP so the actual call can connect
await SipService.instance.reconnect();
}
// Register in main():
// FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);Push Notification Flow Diagrams
iOS APNs VoIP Push Flow
Android FCM Push Flow
SRTP: Encrypting Mobile VoIP Media
Without SRTP
With SRTP + DTLS
// In your WebRTC call setup:
final config = {
'iceServers': [
{'urls': 'stun:stun.celloip.com:3478'},
{
'urls': 'turn:turn.celloip.com:3478',
'username': 'webrtc',
'credential': 'yourTurnPassword',
}
],
'sdpSemantics': 'unified-plan',
// DTLS-SRTP is the default in WebRTC — ensure no plaintext fallback:
'crypto': {
'dtlsSrtpKeyAgreement': true,
}
};
final pc = await createPeerConnection(config);
// dart-sip-ua uses this PC for SRTP-encrypted media automaticallyFrequently Asked Questions
QCan dart-sip-ua handle video calls?
Yes, via flutter_webrtc for the media layer. dart-sip-ua negotiates the video SDP offer/answer and flutter_webrtc renders the video tracks.
QWhat is the battery impact of a VoIP app?
With push-based wake (no persistent WebSocket in background), the battery impact is near zero. The SIP WebSocket only connects when a call is active. A persistent WebSocket registration drains 3–8% extra battery per day.
QDoes this work with both Asterisk and FreeSWITCH?
Yes. dart-sip-ua connects via WebSocket to any compliant SIP server. CelloIP has production Flutter apps registered to both Asterisk PJSIP WSS endpoints and FreeSWITCH mod_sofia WSS profiles.
QHow do you handle CGNAT on mobile networks?
Use WebSocket (wss://) transport — it works through carrier-grade NAT. Combine with ICE/STUN for media. Keep-alive WebSocket pings (30s interval) maintain the connection through NAT mappings.
Related Articles
Build Your Flutter VoIP App
CelloIP Technologies has shipped production Flutter VoIP apps for iOS and Android across 15+ countries — with CallKit, FCM push, SRTP encryption, and white-label branding. Let us build yours.