Back to Blog
FlutterVoIPMobiledart-sip-uaCallKitFCM PushSRTPiOS & Android

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.

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

SIP stack (REGISTER, INVITE, BYE, DTMF)

Cross-platform
flutter_callkit_incoming

Native calling UI (iOS CallKit + Android)

iOS + Android
flutter_webrtc

WebRTC media — SRTP, DTLS, ICE

Cross-platform
firebase_messaging

FCM push for Android background calls

Android
push_notification_ios

APNs VoIP push token registration

iOS
permission_handler

Microphone, camera, notification permissions

Cross-platform

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.

dartsip_service.dart — SIP registration and call setup
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.

dartmain.dart — CallKit incoming call screen
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.

dartbackground_handler.dart — FCM background message handler
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

1
Caller dials extension
2
Asterisk/FS receives INVITE
3
Server sends APNs VoIP push
4
iOS wakes locked device
5
App re-registers SIP
6
CallKit native ring screen

Android FCM Push Flow

1
Caller dials extension
2
Server sends FCM data message
3
FCM wakes app (high-priority)
4
Background isolate starts
5
App re-registers SIP
6
ConnectionService UI shown

SRTP: Encrypting Mobile VoIP Media

Without SRTP

Call audio is plain RTP — unencrypted
Trivial to capture on public Wi-Fi
Wireshark can decode any call in seconds
Violates GDPR and HIPAA for regulated industries
Zero protection on shared mobile networks

With SRTP + DTLS

AES-128/256 encrypted RTP payload
DTLS handshake for automatic key exchange
Enable via flutter_webrtc RTCPeerConnection
Asterisk: media_encryption = dtls in pjsip.conf
FreeSWITCH: SRTP enabled in SIP profile
dartEnabling SRTP via flutter_webrtc RTCPeerConnection
// 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 automatically

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

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.