Skip to content
cloud pbx
  • Cloud PBX
  • Products & Services
    • Cloud PBX
      • Cloud PBX Enterprise
      • Cloud PBX Multi tenant
      • Cloud PBX office
      • Cloud PBX Lite
      • Hotel IP PBX Integrated Hotel PBX Solution
      • Cloud Call Center
        • Cloud call center
        • Call center customer service
        • Call Center Telemarketing
        • Call center AI 
      • SIP Trunk Provider Jakarta–Indonesia
    • Services
      • Data & Integration
        • 3CX, Grafana, and SQL Dashboard Monitoring Optimization
        • Wallboard Custom – Dimension
        • Creomate — Advanced External API Solution for 3CX Integration
        • Universal CRM Integration with 3CX
      • Contact Center Solutions
        • Omni channel Social Media
        • Telemarketing System
        • Solusi Business Process Outsourcing
      • Communication Channel Solutions
        • Microsoft Teams Direct Routing
        • WhatsApp Business Calling API Integration with PBX
        • Video conference: webmeeting 3CX
        • Webrtc softphone
        • Softphone
    • AI Agent & API Integration: 24/7 Automated Customer Service Solution
  • Devices
    • Voice Gateway
      • FXO
        • Dinstar FXO Gateway
        • Yeastar NeoGate TA410 VoIP Gateway
      • FXS
        • Analog Telephony Integration Solution: Dinstar
      • E1
        • E1/T1 Digital VoIP Gateway Newrock OM1000-TE (1–4 E1)
        • Beronet E1 Voip Gateway
    • IP Phone
      • Best IP Phones of 2026
      • SNOM D140, D150
      • Yealink IP Phone series
      • Poly Desk Phone
    • SBC
      • 3CX Micro SBC
      • Linkvil A710 SBC
      • Ribbon SBC
      • Sangoma SBC
  • Case Studies
  • News
  • Video
    • Tutorials
    • Video Integration
    • Testimoni
  • Help Center
  • Contact
  • Partnership Program
  • About Us
cloud pbx
  • Cloud PBX
  • Products & Services
    • Cloud PBX
      • Cloud PBX Enterprise
      • Cloud PBX Multi tenant
      • Cloud PBX office
      • Cloud PBX Lite
      • Hotel IP PBX Integrated Hotel PBX Solution
      • Cloud Call Center
        • Cloud call center
        • Call center customer service
        • Call Center Telemarketing
        • Call center AI 
      • SIP Trunk Provider Jakarta–Indonesia
    • Services
      • Data & Integration
        • 3CX, Grafana, and SQL Dashboard Monitoring Optimization
        • Wallboard Custom – Dimension
        • Creomate — Advanced External API Solution for 3CX Integration
        • Universal CRM Integration with 3CX
      • Contact Center Solutions
        • Omni channel Social Media
        • Telemarketing System
        • Solusi Business Process Outsourcing
      • Communication Channel Solutions
        • Microsoft Teams Direct Routing
        • WhatsApp Business Calling API Integration with PBX
        • Video conference: webmeeting 3CX
        • Webrtc softphone
        • Softphone
    • AI Agent & API Integration: 24/7 Automated Customer Service Solution
  • Devices
    • Voice Gateway
      • FXO
        • Dinstar FXO Gateway
        • Yeastar NeoGate TA410 VoIP Gateway
      • FXS
        • Analog Telephony Integration Solution: Dinstar
      • E1
        • E1/T1 Digital VoIP Gateway Newrock OM1000-TE (1–4 E1)
        • Beronet E1 Voip Gateway
    • IP Phone
      • Best IP Phones of 2026
      • SNOM D140, D150
      • Yealink IP Phone series
      • Poly Desk Phone
    • SBC
      • 3CX Micro SBC
      • Linkvil A710 SBC
      • Ribbon SBC
      • Sangoma SBC
  • Case Studies
  • News
  • Video
    • Tutorials
    • Video Integration
    • Testimoni
  • Help Center
  • Contact
  • Partnership Program
  • About Us
  • English
  • Bahasa Indonesia
  • English
  • Bahasa Indonesia
cloud pbx

IP Phone

8
  • What Is an IP Phone?
  • How to Install/Provision an IP Phone
  • IP Phones Compatible with 3CX
  • Factory Reset an IP Phone: A Required Step Before Provisioning to Avoid Errors

3CX Softphone: Webclient and Apps

12
  • How to Access the 3CX Web Client
  • How to Install and Activate the 3CX Desktop App on Windows (PWA & Windows App)
  • Learn 3CX’s Basic Communication Features in 5 Minutes
  • The Easy Way to Set Available, Away, and DND Status in 3CX
  • Activating the 3CX Mobile App with a QR Code
  • Digital Receptionist (IVR): How to Build a Professional Automated Phone Menu

3CX Admin Console & Manajemen Sistem

22
  • What Is an SBC? Meet the “Security Guard” Protecting Your VoIP System
  • What Is FXO? How FXO Connects Analog Lines to IP PBX
  • Backup and Restore Feature
  • How to Set Up Call Forwarding Based on Presence Status in 3CX
  • How to Use Call Parking and Retrieve Call in 3CX
  • How to Change Your AUX Status (Available, Away, DND) in 3CX
  • Tutorial: Setting Up Call Transcription on Cloud PBX (3CX)
  • Managing Departments & Users in 3CX: Adding Users, Setting Roles, and Managing Departments Easily
  • How to Set Up a Router Phone / 3CX SBC for Remote & WFH Employees
  • Ring Groups vs Call Queues in 3CX: Keep Customers From Waiting and Your Team From Getting Overwhelmed
  • Office Hours & Holiday Rules: Keep Business Calls Handled Even When the Office Is Closed

Keamanan VoIP dan Troubleshoot

6
  • Fixing One-Way Audio in 3CX — Call Connects But Only One Side Can Hear?
  • How to Unblock an IP After Too Many Failed Login Attempts
  • 3CX Firewall Checker: How to Check 3CX Ports So Calls Stay Smooth

Developer

14
  • Transforming Enterprise Phone Architecture with a WebRTC SBC Gateway
  • WebRTC SBC Gateway Integration for Flutter
  • Optimizing Enterprise Collaboration with the 3CX Video Conference API
  • 3CX CRM XML Template Structure & Specification Guide
  • 3CX REST API (XAPI) Endpoint Guide for Configuration
  • Guide to Integrating Odoo CRM with 3CX
  • Flutter WebRTC Softphone App Using the sip_ua Package

SIP TRUNK

8
  • What Is a SIP Trunk?
  • How Many Types of SIP Trunk Numbers or Services Are There?
  • How to Enable Incoming WhatsApp Calls (WhatsApp SIP Calling) in 3CX
  • SIP Trunk Management: How to Connect a Local Provider Phone Number to 3CX

WhatsApp Calling API

12
  • What Is the WhatsApp Calling API?
  • Guide & Optimization: Setting Up WhatsApp Call for Customer Service in 3CX
  • How to Route WhatsApp Calls to Multiple Extensions at Once
  • Guide to Building a WhatsApp Call Queue in 3CX for Customer Service
  • How to Enable Incoming WhatsApp Calls (WhatsApp SIP Calling) in 3CX
  • Integrating WhatsApp Call with CRM Using 3CX
View Categories
  • Home
  • Pusat Bantuan
  • Developer
  • Flutter WebRTC Softphone App Using the sip_ua Package

Flutter WebRTC Softphone App Using the sip_ua Package

Admin
Updated on October 2, 2026

8 min read

================================================

Application                          : Softphone Mobile Client

Module : SIP_UA Flutter Integration

Document                       : Developer Guide

——————————————————————-

Version                                    : v1.0.0

Author         : Solusipbx (Developer Team)

Created              : July 4, 2026

Last Updated            : July 4, 2026

===============================================

Below is a Developer Guide for building a WebRTC softphone app using the sip_ua package in Flutter, with 3CX as the Core PBX. This guide is written so developers can copy, follow, and integrate it directly with a server infrastructure already running on port 443 (Nginx & CoTURN).

🛠️ Step 1: Dependencies & Device Permissions

Open your Flutter project’s pubspec.yaml file, then add the sip_ua package along with the supporting audio library.

YAML


dependencies:
  flutter:
    sdk: flutter
  sip_ua: ^0.7.10 # Use the latest stable version
permission_handler: ^11.3.0 # For handling microphone permission

OS Permission Configuration (Microphone & Internet)

Android (android/app/src/main/AndroidManifest.xml)

Add the following lines inside the <manifest> tag:

XML

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
iOS (ios/Runner/Info.plist)

Add the following key to officially request microphone access on the Apple ecosystem:

XML

<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access to make VoIP phone calls.</string>

Step 2: Controller Class Architecture (sip_helper.dart)

Create a new file named sip_helper.dart. This class wraps all the connection, registration, and call-state handling logic.

Dart

import 'package:sip_ua/sip_ua.dart';
import 'package:permission_handler/permission_handler.dart';

class MySipController implements SipUaHelperListener {
  final SIPUAHelper _helper = SIPUAHelper();

  // Callbacks passed through to the UI
  Function(RegistrationState state)? onRegistrationStateChanged;
  Function(Call call, CallState state)? onCallStateChanged;

  MySipController() {
    // Register the main listener
    _helper.addSipUaHelperListener(this);
  }

  /// Note: 'required String authId' is added to the function parameters
  Future<void> connectAndRegister({
    required String extension,
    required String authId,
    required String password,
    required String domain,
    required String wssUrl,
  }) async {
    // 1. Microphone permission must be granted first
    var status = await Permission.microphone.request();
    if (!status.isGranted) {
      print("Microphone permission denied by user.");
      return;
    }

    // 2. Set up the SIP credential configuration
    UASettings settings = UASettings();
    settings.wsUri = wssUrl;
    settings.uri = 'sip:$extension@$domain';
    settings.authId = authId; // Note: this variable is now valid
    settings.password = password;
    settings.displayName = extension;
    settings.transportType = TransportType.WS;

    // 3. Add your CoTURN configuration on port 443
    settings.pcConfig = {
      'iceServers': [
        {'url': 'stun:$domain:443'},
        {
          'url': 'turn:$domain:443',
          'username': 'admin_coturn',
          'credential': 'passwordcoturn' // password set in turnserver.conf
        }
      ],
      'iceTransportPolicy': 'all'
    };

    // 4. Start the registration process in the background
    _helper.start(settings);
  }

  /// Makes an outbound call
  void makeVoiceCall(String targetExtension) {
    _helper.call(targetExtension, voiceonly: true);
  }

  /// Hangs up or rejects a call
  void hangupOrReject(Call call) {
    call.hangup();
  }

  /// Answers an inbound call
  void answerCall(Call call) {
    call.answer(_helper.buildConstraints());
  }

  // ===========================================================================
  // SIPUAHELPERLISTENER IMPLEMENTATION (EVENT HANDLERS)
  // ===========================================================================

  @override
  void registrationStateChanged(RegistrationState state) {
    print('SIP registration status: ${state.state}');
    if (onRegistrationStateChanged != null) {
      onRegistrationStateChanged!(state);
    }
  }

  @override
  void callStateChanged(Call call, CallState state) {
    print('Call status: ${state.state}');
    if (onCallStateChanged != null) {
      onCallStateChanged!(call, state);
    }
  }

  @override
  void transportStateChanged(TransportState state) {
    print('WebSocket connection status: ${state.state}');
  }

  @override
  void onNewMessage(SIPMessageRequest request) {
    print('Received a new SIP text message');
  }

  @override
  void onNewNotify(Notify notify) {
    print('Received a new notification from the PBX');
  }
}

Step 3: Integrating into the App UI (main.dart)

Here’s a simple UI implementation example for registering an extension, placing calls, and receiving inbound calls in real time.

import 'package:flutter/material.dart';
import 'package:sip_ua/sip_ua.dart';
import 'sip_helper.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const SoftphoneScreen(),
    );
  }
}

class SoftphoneScreen extends StatefulWidget {
  const SoftphoneScreen({super.key});
  @override
  State<SoftphoneScreen> createState() => _SoftphoneScreenState();
}

class _SoftphoneScreenState extends State<SoftphoneScreen> {
  final MySipController _sipController = MySipController();
  final TextEditingController _targetController = TextEditingController();

  String _regStatus = "DISCONNECTED";
  Call? _activeCall;
  String _callStatus = "IDLE";

  @override
  void initState() {
    super.initState();

    // Wire the controller's event listeners to the UI state
    _sipController.onRegistrationStateChanged = (RegistrationState state) {
      setState(() {
        _regStatus = state.state == RegistrationStateEnum.REGISTERED
            ? "CONNECTED  ✔️ "
            : state.state.toString().split('.').last;
      });
    };

    _sipController.onCallStateChanged = (Call call, CallState state) {
      setState(() {
        _activeCall = call;
        _callStatus = state.state.toString().split('.').last;
      });
    };

    // Auto-connect when the app opens
    _sipController.connectAndRegister(
      extension: "101",
      authId: "passwordAuthid",
      password: "passwordExtension",
      domain: "pbx.yourdomain.com",
      wssUrl: "wss://rtc.yourdomain.com/yourappname",
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Alana Hotel Softphone")),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("Extension Status: $_regStatus",
                style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 20),
            Text("Call Status: $_callStatus",
                style: TextStyle(fontSize: 16, color: Colors.blue.shade700)),
            const Divider(height: 40),

            if (_activeCall == null || _callStatus == "CALL_INITIATION" || _callStatus == "IDLE") ...[
              TextField(
                controller: _targetController,
                decoration: const InputDecoration(
                  labelText: "Enter the destination extension number",
                  border: OutlineInputBorder(),
                ),
                keyboardType: TextInputType.number,
              ),
              const SizedBox(height: 10),
              ElevatedButton.icon(
                onPressed: () => _sipController.makeVoiceCall(_targetController.text),
                icon: const Icon(Icons.call),
                label: const Text("Call"),
                style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white),
              ),
            ],

            if (_activeCall != null && _callStatus == "INCOMING") ...[
              Text("Incoming call from: ${_activeCall!.remote_identity}",
                  style: const TextStyle(fontSize: 16, color: Colors.orange)),
              const SizedBox(height: 15),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  ElevatedButton(
                    onPressed: () => _sipController.answerCall(_activeCall!),
                    style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
                    child: const Text("Answer"),
                  ),
                  ElevatedButton(
                    onPressed: () => _sipController.hangupOrReject(_activeCall!),
                    style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                    child: const Text("Reject"),
                  ),
                ],
              )
            ],

            if (_activeCall != null && _callStatus == "CONFIRMED") ...[
              const Icon(Icons.phone_in_talk, size: 60, color: Colors.green),
              const SizedBox(height: 15),
              ElevatedButton.icon(
                onPressed: () => _sipController.hangupOrReject(_activeCall!),
                icon: const Icon(Icons.call_end),
                label: const Text("End Call"),
                style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white),
              )
            ],
          ],
        ),
      ),
    );
  }
}

Important Additional Notes for Developers

  1. Extension URI Format: The settings.uri string must use the full SIP URI format: ‘sip:USERNAME@DOMAIN’. A typo or a missing sip: prefix will cause the handshake to fail.
  2. Audio Handling: The sip_ua package coordinates directly with the phone’s native WebRTC framework. As long as a call is in the CONFIRMED state, audio streaming between microphone and speaker is handled automatically in real time.
  3. Next Step (Push Notifications): If you want this app to keep working while the phone is asleep/off, developers simply need to integrate the MySipController class above with the firebase_messaging (FCM) package and flutter_callkit_incoming, so an incoming call event can instantly wake up the Android/iOS operating system.
  4. When the phone’s operating system goes into power-saving/sleep mode, the WebSocket connection (wss://) that sip_ua maintains to the WebRTC gateway will automatically be dropped by the OS. This add-on section walks developers through integrating Firebase Cloud Messaging (FCM) and Flutter CallKit Incoming to wake the app remotely whenever there’s an incoming call on the 3CX PBX.

What do you need to prepare? Add the following packages below the sip_ua dependency:

dependencies:
  # ... existing sip_ua and permission_handler dependencies ...

  # ADD-ON DEPENDENCIES FOR GOOGLE PUSH
  firebase_core: ^3.0.0
  firebase_messaging: ^15.0.0
  flutter_callkit_incoming: ^1.0.0 # Enables the full-screen call UI while asleep

Additional OS Permission Configuration

Android (android/app/src/main/AndroidManifest.xml)

Add these permissions inside the <manifest> tag to support push notifications and unlocking the lock screen:

XML

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />

iOS (ios/Runner/Info.plist)

Add the following background capabilities (Background Modes):

XML

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

Step 2: Background Handler Logic (main.dart)

Developers must register the background message handler function in main.dart before runApp is called. This function is invoked instantly by Google Play Services even while your app is fully closed.

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';

/// Handles incoming push packets while the app is fully closed/asleep
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();

  // Configure the native OS incoming-call screen (CallKit) parameters
  var params = <String, dynamic>{
    'id': message.data['call_id'] ?? '123',
    'nameCaller': message.data['caller_name'] ?? 'Hotel Call',
    'handle': message.data['caller_extension'] ?? '000',
    'type': 0, // 0 = Voice call (Audio VoIP)
    'duration': 30000, // Ring timeout (30 seconds)
    'android': {
      'isCustomNotification': true,
      'isShowLogo': false,
      'ringtone': 'ringtone_default',
    },
  };

  // Show the full-screen incoming call interface
  await FlutterCallkitIncoming.showCallkitIncoming(params);
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase Core
  await Firebase.initializeApp();

  // Register Google FCM's official background handler
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(const MyApp());

Step 3: Retrieving the Device’s FCM Token

Inside your main UI component, or as soon as the app detects a successful login/registration, the device token must be fetched and sent to your backend webhook server (Creomate).

import 'package:firebase_messaging/firebase_messaging.dart';

void getAndSaveFcmToken(String extensionNumber) async {
  FirebaseMessaging messaging = FirebaseMessaging.instance;

  // 1. Interactively ask the user for push notification permission
  NotificationSettings settings = await messaging.requestPermission(
    alert: true, badge: true, sound: true,
  );

  if (settings.authorizationStatus == AuthorizationStatus.authorized) {
    // 2. Get the unique token from Google's server
    String? token = await messaging.getToken();
    print("FCM Token for extension $extensionNumber: $token");

    // 3. TODO: Developer sends an HTTP POST to the Creomate API / internal database
    // to map: Extension 101 = Token XYZ
    // _apiService.saveTokenToServer(extensionNumber, token);
  }
}

Step 4: Syncing the “Answer Call” Action with sip_ua

When the CallKit screen appears and the user taps the green “Accept” button, the app needs to automatically re-initialize the sip_ua WebSocket connection and answer the call. Add this listener inside your main page’s initState:

import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';

void listenToCallKitActions() {
  FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
    if (event == null) return;

    switch (event.event) {
      case Event.actionCallAccept:
        print("User tapped Answer!");

        // 1. Quickly re-run the WebSocket registration to Janus/3CX
        _sipController.connectAndRegister(
          extension: "101",
          authId: "passwordAuthid",
          password: "passwordExtension",
          domain: "pbx.yourdomain.com",
          wssUrl: "wss://rtc.yourdomain.com/yourappname",
        ).then((_) {
          // 2. Add a short delay/wait for the INCOMING event to fire in sip_ua,
          // then call answerCall() automatically.
        });
        break;

      case Event.actionCallDecline:
        print("User declined the call from the lock screen.");
        // Send a reject signal to the controller if needed
        break;

      default:
        break;
    }
  });
}

[3CX PBX System]

      │

      ▼ (Event: Incoming call to a sleeping extension)

[Webhook Manager / Creomate]

      │

      ▼ (HTTP POST looking up the extension’s FCM token)

[Google FCM Cloud Services]

      │

      ▼ (High-priority push signal)

[Android / iOS Phone (Sleep State)]

      │

      ├─► Wakes up ‘_firebaseMessagingBackgroundHandler’

      ├─► Triggers ‘FlutterCallkitIncoming.showCallkitIncoming’

      └─► User taps Answer ──► WebSocket reconnects ──► WebRTC audio flows

Guide to Integrating Odoo CRM with 3CXTransforming Enterprise Phone Architecture with a WebRTC SBC Gateway

Share This Article :

  • Facebook
  • X
  • LinkedIn

Was it helpful ?

  • Happy
  • Normal
  • Sad
  • Cloud PBX
  • Products & Services
  • Devices
  • Case Studies
  • News
  • Video
  • Help Center
  • Contact
  • Partnership Program
  • About Us
  • Cloud PBX Enterprise
  • Cloud PBX Multi tenant
  • Cloud call center
  • Cloud PBX office
  • Click to Talk
  • Webrtc softphone
  • AI Agent & API Integration: 24/7 Automated Customer Service Solution
  • WhatsApp Business Calling API Integration with PBX
  • Integrasi CRM dengan 3CX
  • Help Center
  • Comply with PCI DSS & HIPAA Standards
  • Practical Ways to Improve Hotel Communication Efficiency
  • Agentic AI: A New Era for Smart Phone Systems
  • Cloud PBX Office for Business Communication
  • Understanding the Call Flow Designer Feature
  • Call Center Hospitality
  • WhatsApp Customer Service with 3CX
  • PBX Monitoring with Grafana and SQL Dashboard
  • Ribbon SBC for Microsoft Teams Direct Routing: SBC 1000 Appliance vs Cloud-Based SWe Lite
  • Sangoma SBC: A Secure Bridge from Telkom SIP Trunk to AI Voice Platforms
  • Privacy Policy

PT. Solusi Teknologi Unggul | My Republik Plaza Wing A Ground Level Zona 6, Green Office Park 6. BSD City Tangerang Banten 15345 Tel:+62-21-50111266

Copyright © 2026 PT. Solusi Teknologi Unggul 

Need help? Chat with us on WhatsApp
WhatsApp
Hello, welcome to Solusipbx.com, thank you for contacting us

To help us provide the right solution, please tell us your Name, Company
and your needs
Open Chat
Powered by Joinchat