omikit-plugin
v4.2.7
Published
Omikit Plugin by ViHAT
Readme
OMICALL SDK for React Native
npm install omikit-pluginThe omikit-plugin enables VoIP/SIP calling via the OMICALL platform with support for both Old and New Architecture (TurboModules + Fabric).
✅ Expo is supported (v4.2.0+) via a config plugin — works with
expo prebuild/ dev-client / EAS Build (not Expo Go, since the SDK ships native code). Verified end-to-end (outbound + inbound calls) on real iOS and Android devices. See Expo Setup. React Native CLI is also fully supported (manual native setup below).
Table of Contents
- Compatibility
- Installation
- Expo Setup
- Android Setup
- iOS Setup
- Architecture Overview
- Quick Start
- Authentication
- Call Flows (ASCII Diagrams)
- API Reference
- Events
- Enums
- Video Calls
- Push Notifications
- Permissions (Android)
- Quality & Diagnostics
- Advanced Features
- Troubleshooting
- License
Compatibility
| omikit-plugin | React Native | Architecture | Installation |
|---------------|--------------|--------------|--------------|
| 4.x (latest) | 0.74+ | Old + New (auto-detect) | npm install omikit-plugin |
| 3.3.x (legacy) | 0.60 – 0.73 | Old Architecture only | npm install [email protected] |
v4.0.x highlights:
- TurboModules (JSI) — 4-10x faster native method calls via direct C++ bridge
- 100% backward compatible — auto-detects architecture at runtime
- Zero breaking changes from v3.x for RN 0.74+
- Bridgeless mode support for full New Architecture (iOS & Android)
Native SDK Versions
| Platform | SDK | Version | |----------|-----|---------| | Android | omi-sdk | 2.8.17 | | iOS | OmiKit | 1.11.29 |
Platform Requirements
| | Android | iOS | |--|---------|-----| | Min SDK | API 24 (Android 7.0) | iOS 13.0 | | Target SDK | API 36 (Android 16) | — | | Compile SDK | API 36 | — |
Device Requirements
| Requirement | Platform | Notes | |-------------|----------|-------| | Physical device | iOS (required) | iOS Simulator is not supported — OmiKit binary is arm64 device-only | | Physical device | Android (recommended) | Emulator works for basic UI testing but VoIP/audio routing is unreliable | | Google Play Services | Android (required) | Required for FCM push notifications | | Microphone | Both (required) | Required for all calls | | Camera | Both (optional) | Only required for video calls | | Internet | Both (required) | SIP registration + RTP media streaming |
Package Size
| Component | Size | |-----------|------| | npm package (total) | ~353 KB | | Android native code | ~4.7 MB | | iOS native code | ~176 KB |
Note: These sizes are for the plugin only. The native SDKs (OmiKit/OMIKIT) are installed separately via CocoaPods/Maven and will add to the final app size.
Installation
npm install omikit-plugin
# or
yarn add omikit-pluginiOS
cd ios && pod installAndroid
No extra steps — permissions are declared in the module's AndroidManifest.xml.
Expo Setup
For Expo projects using prebuild / dev-client / EAS Build. Requires a custom dev client (not Expo Go) because the SDK ships native code — this is normal for any native library. If you use React Native CLI (bare workflow), skip this section and follow Android Setup / iOS Setup instead.
1. Install
npx expo install omikit-plugin2. Add the config plugin to app.json
The plugin automates all native setup (permissions, background modes, Push capability, incoming-call intent-filter, MainActivity attributes, maven repos, and OmiKit runtime init) — you do not edit AppDelegate, MainActivity, Info.plist, or AndroidManifest by hand.
{
"expo": {
"plugins": [
[
"omikit-plugin",
{
"environment": "production",
"enableVideo": false,
"callKitImage": "call_image",
"maxCall": 1,
"microphonePermission": "This app needs microphone access for voice calls.",
"cameraPermission": "This app needs camera access for video calls."
}
]
]
}
}3. Android — Maven credentials & Kotlin version
The Android SDK (io.omicrm.vihat:omi-sdk) is served from GitHub Packages (private), so Gradle needs credentials. The config plugin adds the repo with a credentials block that reads OMI_USER / OMI_TOKEN from the environment (or a gradle property) — set them before building:
export OMI_USER=omicall
export OMI_TOKEN=<omi_github_packages_token>Contact the OMICall development team to get
OMI_USER/OMI_TOKEN. On EAS Build, add them as secrets. You can also put them inandroid/gradle.properties(keep it gitignored — never commit the token).
Match your Kotlin version to your React Native version (RN 0.76 → 1.9.24) via expo-build-properties to avoid a Compose-compiler mismatch:
[
"expo-build-properties",
{ "android": { "kotlinVersion": "1.9.24" } }
]4. Prebuild & run
npx expo prebuild --clean
npx expo run:ios # iOS — use a physical device to test VoIP push
OMI_USER=omicall OMI_TOKEN=… npx expo run:android # Android needs the tokenOn EAS Build, no extra steps besides the secrets above — the plugin runs during the prebuild phase.
Plugin options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| environment | 'sandbox' \| 'production' | 'production' | OmiKit environment |
| enableVideo | boolean | false | Enables camera permission, NSCameraUsageDescription, and video-view interop. Leave false for audio-only apps |
| userNameKey | string | 'full_name' | OmiKit userNameKey |
| maxCall | number | 1 | Max concurrent calls |
| callKitImage | string | 'call_image' | iOS CallKit image asset name |
| typePushVoip | 'default' \| 'callkit' | 'default' | VoIP push type |
| microphonePermission | string | (generic) | iOS NSMicrophoneUsageDescription message |
| cameraPermission | string | (generic) | iOS NSCameraUsageDescription message (video only) |
| apsEnvironment | 'development' \| 'production' | 'development' | iOS aps-environment entitlement |
| onPremise | object | — | On-premise endpoint overrides (see below) |
On-premise (self-hosted) config
For customers routing traffic to their own infrastructure, pass an onPremise object. Every field is optional — omit a field to keep the SDK default (OMI cloud). The SDK applies these before its first request at launch.
[
"omikit-plugin",
{
"environment": "production",
"onPremise": {
"mobileSdkHost": "omisdk.your-domain.com",
"publicApiHost": "public.your-domain.com",
"pushInfoHost": "push-info.your-domain.com",
"logUploadHost": "log.your-domain.com",
"sipProxy": "sip.your-domain.com",
"stunServer": "stun.your-domain.com",
"turnServer": "turn.your-domain.com",
"turnUsername": "your-turn-user",
"turnPassword": "your-turn-pass"
}
}
]⚠️ Secrets:
app.jsonis committed to git. Do not put a realturnPasswordthere for production — inject it via an env-driven prebuild config (e.g.app.config.jsreadingprocess.env) or EAS secrets.
Firebase / FCM
Android inbound calls require FCM (iOS uses PushKit, handled natively — no Firebase needed for iOS calls). Firebase is your dependency — omikit-plugin does not bundle it. Install it and let its own config plugin wire the native side:
- Install:
npx expo install @react-native-firebase/app @react-native-firebase/messaging - Supply your Google services files:
google-services.json(Android) andGoogleService-Info.plist(iOS). - Fetch the FCM token via
messaging().getToken()and pass it to the SDK at login.
Declare everything in app.json (see the combined block below).
Use
useFrameworks: "dynamic"— not"static".@react-native-firebaseon iOS needsuse_frameworks!. Choose dynamic: Firebase v21 supports it and OmiKit ships as a dynamic xcframework, so it builds out of the box."static"triggersGoogleUtilities does not define modulesand (on New Architecture)Redefinition of module 'ReactCommon'— do not use it.You do not need to create any custom config plugin (no
plugins/*.jsfile). Every plugin below comes from npm and is referenced by name.
Full Expo app.json (copy-paste)
{
"expo": {
"ios": {
"bundleIdentifier": "your.bundle.id",
"googleServicesFile": "./GoogleService-Info.plist"
},
"android": {
"package": "your.package.name",
"googleServicesFile": "./google-services.json"
},
"plugins": [
["expo-build-properties", { "ios": { "useFrameworks": "dynamic" } }],
["react-native-permissions", { "iosPermissions": ["Microphone"] }],
"@react-native-firebase/app",
"@react-native-firebase/messaging",
["omikit-plugin", { "environment": "production" }]
]
}
}Then npx expo prebuild --clean && npx expo run:ios. See expo-example/ for a complete, working setup (its google-services.json / GoogleService-Info.plist are gitignored — supply your own).
Microphone permission:
react-native-permissionswithiosPermissions: ["Microphone"]is required — it generatessetup_permissions(['Microphone'])in the Podfile so the SDK can request the mic. Without it, iOS reports the mic asunavailableand calls fail.
Video calls on New Architecture
Video views (OmiLocalCameraView / OmiRemoteCameraView) are legacy ViewManagers and require bridge mode. If you enable enableVideo on New Architecture, do not enable React Native bridgeless mode, or video won't render.
How it works (no AppDelegate/MainActivity edits)
OmiKit's runtime init runs via native lifecycle hooks shipped in the SDK, so there is nothing to inject into your AppDelegate / MainActivity and nothing breaks across RN/Expo upgrades:
- iOS —
OmikitExpoAppDelegateBridge(an Objective-C class in the pod) registers itself as an Expo AppDelegate subscriber at load time (+load) and runs the OmiKit init (CallKit provider, PushKit registry,setEnviroment, on-premise, notification-center delegate), reading its config from the Info.plist keys the config plugin writes. On a bare React Native app (no ExpoModulesCore) it detects Expo is absent and no-ops, so your existing AppDelegate integration is untouched. - Android — Expo autolinking discovers the plugin via
expo-module.config.jsonand registersOmikitReactActivityLifecycleListener, which forwardsonResume/onNewIntentto the SDK.
Verified end-to-end on real iOS and Android devices — expo prebuild + build + launch shows the SDK initialising automatically ([OMI NATIVE] +load … → didFinishLaunching — init OmiKit), and both outbound and inbound calls work.
Android Setup
Note: This section is for React Native CLI (bare) projects. Expo projects should use Expo Setup instead — the config plugin does all of this automatically.
1. Permissions
Add to android/app/src/main/AndroidManifest.xml:
<!-- Required for all calls -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- Only required for video calls -->
<uses-permission android:name="android.permission.CAMERA" />Note: If your app does NOT use video calls, add the following to your app's
AndroidManifest.xmlto remove the camera foreground service permission declared by the SDK:<!-- Remove camera foreground service if NOT using video call --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" tools:node="remove" />
Note: By default, the SDK declares
WRITE_CALL_LOGpermission to save calls to the device's call history. If your app does NOT want calls saved to the device call log, add the following to remove it:<!-- Remove if you do NOT want calls saved to device call history --> <uses-permission android:name="android.permission.WRITE_CALL_LOG" tools:node="remove" />Make sure to add the
toolsnamespace to your manifest tag:xmlns:tools="http://schemas.android.com/tools"
2. Incoming Call Activity (Required)
Your main Activity must handle incoming call intents from the SDK. Add the following intent-filter to your MainActivity in AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:launchMode="singleTask"
...>
<!-- Incoming call intent-filter (required for lock screen) -->
<intent-filter>
<action android:name="${applicationId}.ACTION_INCOMING_CALL" />
<action android:name="android.intent.action.CALL" />
<category android:name="android.intent.category.DEFAULT" />
<data android:host="incoming_call" android:scheme="omisdk" />
</intent-filter>
</activity>Important: The
${applicationId}.ACTION_INCOMING_CALLaction ensures incoming calls show correctly on lock screen for Android 9-14. Without this, the default dialer may intercept the intent instead of your app.
3. Firebase Cloud Messaging (FCM)
Add your google-services.json to android/app/.
In android/app/build.gradle:
apply plugin: 'com.google.gms.google-services'4. Maven Repository
Option A — settings.gradle.kts (recommended for new projects)
// settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
maven {
url = uri("https://maven.pkg.github.com/omicall/OMICall-SDK")
credentials {
username = providers.gradleProperty("OMI_USER").getOrElse("")
password = providers.gradleProperty("OMI_TOKEN").getOrElse("")
}
authentication {
create<BasicAuthentication>("basic")
}
}
}
}Option B — build.gradle (Groovy / legacy projects)
// android/build.gradle (project level)
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
maven {
url "https://maven.pkg.github.com/omicall/OMICall-SDK"
credentials {
username = project.findProperty("OMI_USER") ?: ""
password = project.findProperty("OMI_TOKEN") ?: ""
}
authentication {
basic(BasicAuthentication)
}
}
}
}Then add your credentials to ~/.gradle/gradle.properties (or project-level gradle.properties):
OMI_USER=omi_github_username
OMI_TOKEN=omi_github_access_tokenNote: Contact the OMICall development team to get
OMI_USERandOMI_TOKENcredentials.
5. New Architecture (Optional)
To enable New Architecture on Android, in android/gradle.properties:
newArchEnabled=true6. Code Shrinking (R8 / ProGuard)
No configuration needed. Turning on minifyEnabled true in your release build works
out of the box from v4.2.7 — the plugin ships its ProGuard rules through
consumerProguardFiles, and the OMI Android SDK ships its own, so your app inherits both
automatically:
// android/app/build.gradle — this is enough
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}Do not copy plugin rules into your own proguard-rules.pro; they are applied for you.
R8 removes and renames code it believes is unused. Several things here are reached only by name at runtime, so R8 cannot see the reference and would strip them — the app then fails at runtime with no build-time warning:
| Kept | Why |
|------|-----|
| com.omikitplugin.** | React Native instantiates native modules and view managers reflectively and looks them up by the string returned from getName() |
| com.omikitplugin.expo.OmikitExpoPackage | Expo autolinking loads it by the class name written in expo-module.config.json |
| vn.vihat.omicall.**, net.gotev.sipservice.**, org.pjsip.** | SIP stack and SDK entry points resolved by name (shipped by the OMI SDK) |
| retrofit2.Call, retrofit2.Response, generic signatures | Retrofit reads the generic type argument at call time; stripping it throws ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType |
| SDK data models | Gson maps JSON keys to field names; renaming a field silently turns parsed values into null |
If you are on a version older than 4.2.7 and cannot upgrade yet, add these to your app's
proguard-rules.pro as a stopgap:
-keep class com.omikitplugin.** { *; }
-keep class vn.vihat.omicall.** { *; }
-keep class net.gotev.sipservice.** { *; }
-keep class org.pjsip.** { *; }
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-keepattributes Signature, *Annotation*, InnerClasses, EnclosingMethodiOS Setup
1. Info.plist
Add to your Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>Required for VoIP calls</string>
<key>NSCameraUsageDescription</key>
<string>Required for video calls</string>2. Background Modes
In Xcode, enable the following Background Modes:
- [x] Voice over IP
- [x] Remote notifications
- [x] Background fetch
3. Push Notifications
Enable Push Notifications capability in Xcode for VoIP push (PushKit).
4. AppDelegate Setup
The AppDelegate template differs depending on your React Native version. Choose the one that matches your project:
RN 0.74 – 0.78 (RCTAppDelegate pattern)
#import <RCTAppDelegate.h>
#import <UIKit/UIKit.h>
#import <UserNotifications/UserNotifications.h>
#import <OmiKit/OmiKit.h>
@interface AppDelegate : RCTAppDelegate <UIApplicationDelegate, RCTBridgeDelegate, UNUserNotificationCenterDelegate>
@property (nonatomic, strong) UIWindow *window;
@property (nonatomic, strong) PushKitManager *pushkitManager;
@property (nonatomic, strong) CallKitProviderDelegate *provider;
@property (nonatomic, strong) PKPushRegistry *voipRegistry;
@end#import "AppDelegate.h"
#import <Firebase.h>
#import <React/RCTBundleURLProvider.h>
#import <OmiKit/OmiKit.h>
#if __has_include("OmikitNotification.h")
#import "OmikitNotification.h"
#else
#import <omikit_plugin/OmikitNotification.h>
#endif
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"YourAppName"; // Replace with your app name
// ----- OmiKit Config ------
[OmiClient setEnviroment:KEY_OMI_APP_ENVIROMENT_SANDBOX
userNameKey:@"full_name"
maxCall:2
callKitImage:@"call_image"
typePushVoip:TYPE_PUSH_CALLKIT_DEFAULT];
self.provider = [[CallKitProviderDelegate alloc]
initWithCallManager:[OMISIPLib sharedInstance].callManager];
self.voipRegistry = [[PKPushRegistry alloc]
initWithQueue:dispatch_get_main_queue()];
self.pushkitManager = [[PushKitManager alloc]
initWithVoipRegistry:self.voipRegistry];
if (@available(iOS 10.0, *)) {
[UNUserNotificationCenter currentNotificationCenter].delegate =
(id<UNUserNotificationCenterDelegate>)self;
}
if ([FIRApp defaultApp] == nil) {
[FIRApp configure];
}
// ----- End OmiKit Config ------
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
// Handle foreground notifications
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
completionHandler(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge);
}
// Handle missed call notification tap
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)())completionHandler
{
NSDictionary *userInfo = response.notification.request.content.userInfo;
if (userInfo && [userInfo valueForKey:@"omisdkCallerNumber"]) {
[OmikitNotification didRecieve:userInfo];
}
completionHandler();
}
// Register push notification token
- (void)application:(UIApplication *)app
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken
{
const unsigned char *data = (const unsigned char *)[devToken bytes];
NSMutableString *token = [NSMutableString string];
for (NSUInteger i = 0; i < [devToken length]; i++) {
[token appendFormat:@"%02.2hhX", data[i]];
}
[OmiClient setUserPushNotificationToken:[token copy]];
}
// Terminate all calls when app is killed
- (void)applicationWillTerminate:(UIApplication *)application {
@try {
[OmiClient OMICloseCall];
} @catch (NSException *exception) {}
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self bundleURL];
}
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
@endRN 0.79+ (RCTReactNativeFactory pattern)
RN 0.79+ uses RCTReactNativeFactory instead of RCTAppDelegate. Add OmiKit setup in your existing AppDelegate:
import UIKit
import React
import ReactAppDependencyProvider
import OmiKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
var provider: CallKitProviderDelegate?
var pushkitManager: PushKitManager?
var voipRegistry: PKPushRegistry?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// React Native setup
let delegate = ReactNativeDelegate()
let factory = RCTReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "YourAppName",
in: window,
launchOptions: launchOptions
)
// ----- OmiKit Config ------
#ifdef DEBUG
OmiClient.setEnviroment(KEY_OMI_APP_ENVIROMENT_SANDBOX,
userNameKey: "full_name",
maxCall: 1,
callKitImage: "call_image",
typePushVoip: TYPE_PUSH_CALLKIT_DEFAULT)
#else
OmiClient.setEnviroment(KEY_OMI_APP_ENVIROMENT_PRODUCTION,
userNameKey: "full_name",
maxCall: 1,
callKitImage: "call_image",
typePushVoip: TYPE_PUSH_CALLKIT_DEFAULT)
#endif
provider = CallKitProviderDelegate(callManager: OMISIPLib.sharedInstance().callManager)
voipRegistry = PKPushRegistry(queue: .main)
pushkitManager = PushKitManager(voipRegistry: voipRegistry!)
UNUserNotificationCenter.current().delegate = self
FirebaseApp.configure()
// ----- End OmiKit Config ------
return true
}
// Handle missed call notification tap
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
if userInfo["omisdkCallerNumber"] != nil {
OmikitNotification.didRecieve(userInfo)
}
completionHandler()
}
// Register push notification token
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhX", $0) }.joined()
OmiClient.setUserPushNotificationToken(token)
}
// Terminate all calls when app is killed
func applicationWillTerminate(_ application: UIApplication) {
try? OmiClient.omiCloseCall()
}
}
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
override func sourceURL(for bridge: RCTBridge) -> URL? {
return bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}#import "AppDelegate.h"
#import <Firebase.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTReactNativeFactory.h>
#import <ReactAppDependencyProvider/RCTAppDependencyProvider.h>
#import <OmiKit/OmiKit.h>
#if __has_include("OmikitNotification.h")
#import "OmikitNotification.h"
#else
#import <omikit_plugin/OmikitNotification.h>
#endif
@interface ReactNativeDelegate : RCTDefaultReactNativeFactoryDelegate
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
ReactNativeDelegate *delegate = [ReactNativeDelegate new];
RCTReactNativeFactory *factory = [[RCTReactNativeFactory alloc] initWithDelegate:delegate];
delegate.dependencyProvider = [RCTAppDependencyProvider new];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[factory startReactNativeWithModuleName:@"YourAppName" in:self.window launchOptions:launchOptions];
// ----- OmiKit Config ------
#ifdef DEBUG
[OmiClient setEnviroment:KEY_OMI_APP_ENVIROMENT_SANDBOX userNameKey:@"full_name" maxCall:1 callKitImage:@"icYourApp" typePushVoip:@"background"];
#else
[OmiClient setEnviroment:KEY_OMI_APP_ENVIROMENT_PRODUCTION userNameKey:@"full_name" maxCall:1 callKitImage:@"icYourApp" typePushVoip:@"background"];
#endif
self.provider = [[CallKitProviderDelegate alloc]
initWithCallManager:[OMISIPLib sharedInstance].callManager];
self.voipRegistry = [[PKPushRegistry alloc]
initWithQueue:dispatch_get_main_queue()];
self.pushkitManager = [[PushKitManager alloc]
initWithVoipRegistry:self.voipRegistry];
if (@available(iOS 10.0, *)) {
[UNUserNotificationCenter currentNotificationCenter].delegate =
(id<UNUserNotificationCenterDelegate>)self;
}
if ([FIRApp defaultApp] == nil) {
[FIRApp configure];
}
// ----- End OmiKit Config ------
return YES;
}
// Handle missed call notification tap
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)())completionHandler
{
NSDictionary *userInfo = response.notification.request.content.userInfo;
if (userInfo && [userInfo valueForKey:@"omisdkCallerNumber"]) {
[OmikitNotification didRecieve:userInfo];
}
completionHandler();
}
// Register push notification token
- (void)application:(UIApplication *)app
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken
{
const unsigned char *data = (const unsigned char *)[devToken bytes];
NSMutableString *token = [NSMutableString string];
for (NSUInteger i = 0; i < [devToken length]; i++) {
[token appendFormat:@"%02.2hhX", data[i]];
}
[OmiClient setUserPushNotificationToken:[token copy]];
}
// Terminate all calls when app is killed
- (void)applicationWillTerminate:(UIApplication *)application {
@try {
[OmiClient OMICloseCall];
} @catch (NSException *exception) {}
}
@end
@implementation ReactNativeDelegate
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self bundleURL];
}
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
@endNote: Replace
YourAppNamewith your app's module name. For production, changeKEY_OMI_APP_ENVIROMENT_SANDBOXtoKEY_OMI_APP_ENVIROMENT_PRODUCTION.
5. New Architecture (Optional)
In your Podfile:
ENV['RN_NEW_ARCH_ENABLED'] = '1'For New Architecture with video call support, add Fabric interop registration in AppDelegate.mm inside didFinishLaunchingWithOptions, before return [super ...]:
// Required imports at the top of AppDelegate.mm
#import <React-RCTFabric/React/RCTComponentViewFactory.h>
#import <React-RCTFabric/React/RCTLegacyViewManagerInteropComponentView.h>
// Inside didFinishLaunchingWithOptions, before return:
[RCTLegacyViewManagerInteropComponentView supportLegacyViewManagerWithName:@"OmiLocalCameraView"];
[RCTLegacyViewManagerInteropComponentView supportLegacyViewManagerWithName:@"OmiRemoteCameraView"];Important: Bridgeless mode is not yet supported for video call views. If you use New Architecture, keep bridge mode enabled (do not add
bridgelessEnabledreturningYES).
Then run cd ios && pod install.
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ React Native App │
│ │
│ import { startCall, omiEmitter } from 'omikit-plugin' │
└──────────────────────────┬──────────────────────────────────┘
│
┌────────────▼────────────┐
│ Architecture Bridge │
│ │
│ TurboModule? ──► JSI │ (New Arch: direct C++ calls)
│ │ │
│ └──► NativeModule │ (Old Arch: JSON bridge)
└────────────┬────────────┘
│
┌────────────────┼────────────────┐
│ │
┌──────▼──────┐ ┌───────▼──────┐
│ Android │ │ iOS │
│ │ │ │
│ OmikitPlugin│ │ OmikitPlugin │
│ Module.kt │ │ .swift │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ OMIKIT SDK │ │ OmiKit SDK │
│ (v2.6.5) │ │ (v1.11.4) │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ SIP Stack │ │ SIP Stack │
│ (OMSIP) │ │ (OMSIP) │
└─────────────┘ └──────────────┘Quick Start
import {
startServices,
initCallWithUserPassword,
startCall,
joinCall,
endCall,
omiEmitter,
OmiCallEvent,
OmiCallState,
} from 'omikit-plugin';
// Step 1: Start SDK services
// ⚠️ Call ONCE on app launch (e.g., in App.tsx / index.js / useEffect in root component)
// Do NOT call this multiple times — it initializes native audio and event listeners.
await startServices();
// Step 2: Login with SIP credentials
const loginResult = await initCallWithUserPassword({
userName: 'sip_user',
password: 'sip_password',
realm: 'your_realm',
host: '', // SIP proxy, defaults to vh.omicrm.com
isVideo: false,
fcmToken: 'your_fcm_token',
projectId: 'your_project_id', // firebase project id
});
// Step 3: Listen to call events
const subscription = omiEmitter.addListener(
OmiCallEvent.onCallStateChanged,
(data) => {
console.log('Call state:', data.status);
switch (data.status) {
case OmiCallState.incoming:
// Show incoming call UI
// data.callerNumber, data.isVideo
break;
case OmiCallState.confirmed:
// Call connected — show active call UI
break;
case OmiCallState.disconnected:
// Call ended
// data.codeEndCall — SIP end code
break;
}
}
);
// Step 4: Make outgoing call
const result = await startCall({
phoneNumber: '0901234567',
isVideo: false,
});
if (result.status === 8) {
console.log('Call started, ID:', result._id);
}
// Step 5: Accept incoming call
await joinCall();
// Step 6: End call
await endCall();
// Cleanup on unmount
subscription.remove();Authentication
Two authentication methods are available. Each supports two login modes depending on who is using the app:
| Mode | isSkipDevices | Use Case | Capabilities |
|------|-----------------|----------|--------------|
| Agent (default) | false | Employees / call center agents | Can make outbound calls to any telecom number |
| Customer | true | End customers | Can only call the business hotline (no outbound to external numbers) |
Option 1: Username + Password (SIP Credentials)
await initCallWithUserPassword({
userName: string, // SIP username
password: string, // SIP password
realm: string, // SIP realm/domain
host?: string, // SIP proxy server (optional)
isVideo: boolean, // Enable video capability
fcmToken: string, // Firebase token for push notifications
projectId: string, // Firebase project ID
isSkipDevices?: boolean, // true = Customer mode, false = Agent mode (default)
});Agent Login (default)
For employees / call center agents who can make outbound calls to any phone number:
await initCallWithUserPassword({
userName: '100',
password: 'sip_password',
realm: 'your_realm',
host: '',
isVideo: false,
fcmToken: fcmToken,
// isSkipDevices defaults to false — Agent mode
});Customer Login
For end customers who can only call the business hotline — no outbound dialing to external telecom numbers, no assigned phone number:
await initCallWithUserPassword({
userName: '200',
password: 'sip_password',
realm: 'your_realm',
host: '',
isVideo: false,
fcmToken: fcmToken,
isSkipDevices: true, // Customer mode — skip device registration
});Option 2: API Key
await initCallWithApiKey({
fullName: string, // Display name
usrUuid: string, // User UUID from OMICALL
apiKey: string, // API key from OMICALL dashboard
isVideo: boolean, // Enable video capability
phone: string, // Phone number
fcmToken: string, // Firebase token for push notifications
projectId?: string, // OMICALL project ID (optional)
});Option 3: App-to-App API (v4.0+)
Starting from v4.0, customers using the App-to-App service must call the OMICALL API to provision SIP extensions before initializing the SDK. The API returns SIP credentials that you pass to initCallWithUserPassword() with isSkipDevices: true.
For full API documentation (endpoints, request/response formats), see the API Integration Guide.
Quick flow:
Your Backend OMICALL API Mobile App (SDK)
│ │ │
│ 1. POST .../init │ │
├─────────────────────────────►│ │
│ {domain, extension, │ │
│ password, proxy} │ │
│◄─────────────────────────────┤ │
│ │ │
│ 2. Return credentials │ │
├──────────────────────────────────────────────────────────►│
│ │ 3. startServices() │
│ │ 4. initCallWithUserPassword
│ │ (isSkipDevices: true) │
│ │ │// After getting credentials from your backend:
await startServices();
await initCallWithUserPassword({
userName: credentials.extension, // from API response
password: credentials.password, // from API response
realm: credentials.domain, // from API response
host: credentials.outboundProxy, // from API response
isVideo: false,
fcmToken: 'your-fcm-token',
isSkipDevices: true, // Required for App-to-App
});Important:
- Call the OMICALL API from your backend server only — never expose the Bearer token in client-side code.
- You must call the Logout API before switching users. Otherwise, both devices using the same SIP extension will receive incoming calls simultaneously.
- Use getter functions (
getProjectId(),getAppId(),getDeviceId(),getFcmToken(),getVoipToken()) to retrieve device params for the Add Device and Logout APIs.
Call Flows
Outgoing Call Flow
┌──────────┐ ┌──────────┐ ┌──────────┐
│ JS App │ │ Native │ │ SIP/PBX │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ startCall() │ │
├────────────────────►│ │
│ │ SIP INVITE │
│ ├────────────────────►│
│ │ │
│ calling (1) │ 180 Ringing │
│◄────────────────────┤◄────────────────────┤
│ │ │
│ early (3) │ 183 Progress │
│◄────────────────────┤◄────────────────────┤
│ │ │
│ connecting (4) │ 200 OK │
│◄────────────────────┤◄────────────────────┤
│ │ │
│ confirmed (5) │ ACK │
│◄────────────────────┤────────────────────►│
│ │ │
│ ══════ Active Call (RTP audio/video) ══════
│ │ │
│ endCall() │ │
├────────────────────►│ BYE │
│ ├────────────────────►│
│ disconnected (6) │ 200 OK │
│◄────────────────────┤◄────────────────────┤
│ │ │Incoming Call — App in Foreground
┌──────────┐ ┌──────────┐ ┌──────────┐
│ JS App │ │ Native │ │ SIP/PBX │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ │ SIP INVITE │
│ │◄────────────────────┤
│ │ 180 Ringing │
│ ├────────────────────►│
│ │ │
│ incoming (2) │ │
│◄────────────────────┤ (event emitted) │
│ │ │
│ ┌──────────────┐ │ │
│ │ Show Call UI │ │ │
│ │[Accept][Deny]│ │ │
│ └──────────────┘ │ │
│ │ │
│ joinCall() │ │
├────────────────────►│ 200 OK │
│ ├────────────────────►│
│ confirmed (5) │ ACK │
│◄────────────────────┤◄────────────────────┤
│ │ │
│ ══════ Active Call (RTP audio/video) ══════
│ │ │Incoming Call — App in Background / Killed
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ JS App │ │ Native │ │Push Svc │ │ SIP/PBX │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ │ │ Push Notify │
│ │ │◄──────────────┤
│ │ │ │
┌───────────────── iOS (VoIP Push) ───────────────────┐
│ │ │ │ │ │
│ │ │ PushKit VoIP │ │ │
│ │ │◄──────────────┤ │ │
│ │ │ │ │ │
│ │ │ Show CallKit │ │ │
│ │ │ ┌──────────┐ │ │ │
│ │ │ │ System │ │ │ │
│ │ │ │ Call UI │ │ │ │
│ │ │ │[Slide ►] │ │ │ │
│ │ │ └──────────┘ │ │ │
│ │ │ │ │ │
│ │ App launched │ │ │ │
│ │◄──────────────┤ │ │ │
│ │ incoming (2) │ │ │ │
│ │◄──────────────┤ │ │ │
└─────────────────────────────────────────────────────┘
┌───────────────── Android (FCM) ─────────────────────┐
│ │ │ │ │ │
│ │ │ FCM Message │ │ │
│ │ │◄──────────────┤ │ │
│ │ │ │ │ │
│ │ │ Start Foreground Service │ │
│ │ │ ┌──────────────────────┐ │ │
│ │ │ │ Full-screen Notif │ │ │
│ │ │ │ [Accept] [Decline] │ │ │
│ │ │ └──────────────────────┘ │ │
│ │ │ │ │ │
│ │ App launched │ │ │ │
│ │◄──────────────┤ │ │ │
│ │ incoming (2) │ │ │ │
│ │◄──────────────┤ │ │ │
└─────────────────────────────────────────────────────┘
│ │ │ │
│ joinCall() │ │ │
├──────────────►│ 200 OK │ │
│ ├──────────────────────────────►│
│ confirmed (5)│ │ │
│◄──────────────┤ │ │
│ │ │ │Missed Call Flow
┌──────────┐ ┌──────────┐ ┌──────────┐
│ JS App │ │ Native │ │ SIP/PBX │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ │ SIP INVITE │
│ │◄────────────────────┤
│ incoming (2) │ │
│◄────────────────────┤ │
│ │ │
│ (user ignores / timeout / caller hangs up)
│ │ │
│ │ CANCEL │
│ │◄────────────────────┤
│ disconnected (6) │ 200 OK │
│◄────────────────────┤────────────────────►│
│ │ │
│ │ Show Missed Call │
│ │ Notification │
│ │ │
│ (user taps notif) │ │
│ │ │
│ onClickMissedCall │ │
│◄────────────────────┤ │
│ │ │Call Transfer Flow
┌──────────┐ ┌──────────┐ ┌──────────┐
│ JS App │ │ Native │ │ SIP/PBX │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ ══════ Active Call with Party A ══════
│ │ │
│ transferCall(B) │ │
├────────────────────►│ SIP REFER → B │
│ ├────────────────────►│
│ │ │
│ │ 202 Accepted │
│ │◄────────────────────┤
│ │ │
│ disconnected (6) │ BYE (from A) │
│◄────────────────────┤◄────────────────────┤
│ │ │
│ ══════ Party A now talks to B ══════
│ │ │Reject / Drop Call Flow
┌──────────┐ ┌──────────┐ ┌──────────┐
│ JS App │ │ Native │ │ SIP/PBX │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ incoming (2) │ SIP INVITE │
│◄────────────────────┤◄────────────────────┤
│ │ │
┌── rejectCall() ───┐ │
│ Decline this │ 486 Busy Here │
│ device only ├─────────────────────────► │
└───────────────────┘ (other devices ring) │
│ │ │
┌── dropCall() ─────┐ │
│ Decline + stop │ 603 Decline │
│ ALL devices ├─────────────────────────► │
└───────────────────┘ (PBX stops all ringing) │
│ │ │API Reference
Service & Auth
| Function | Returns | Description |
|----------|---------|-------------|
| startServices() | Promise<boolean> | Initialize SDK. Call once on app launch (e.g., App.tsx or index.js). Do not call multiple times |
| initCallWithUserPassword(data) | Promise<boolean> | Login with SIP username/password |
| initCallWithApiKey(data) | Promise<boolean> | Login with API key |
| logout() | Promise<boolean> | Logout and unregister SIP |
| logoutAndWait() | Promise<boolean> | (v4.1.7+) Logout that resolves only after the SDK finishes the backend devices/remove HTTP call and clears local state. Use before an immediate re-login to avoid race conditions |
Call Control
| Function | Returns | Description |
|----------|---------|-------------|
| startCall({ phoneNumber, isVideo }) | Promise<{ status, message, _id }> | Initiate outgoing call |
| startCallWithUuid({ usrUuid, isVideo }) | Promise<boolean> | Call by user UUID |
| joinCall() | Promise<any> | Accept incoming call |
| endCall() | Promise<any> | End active call (sends SIP BYE) |
| rejectCall() | Promise<boolean> | Reject on this device only (486) |
| dropCall() | Promise<boolean> | Reject + stop ringing on ALL devices (603) |
| transferCall({ phoneNumber }) | Promise<boolean> | Blind transfer to another number |
| getInitialCall() | Promise<any> | Get pending call data on cold start |
Media Control
| Function | Returns | Description |
|----------|---------|-------------|
| toggleMute() | Promise<boolean\|null> | Toggle microphone mute |
| toggleSpeaker() | Promise<boolean> | Toggle speakerphone |
| toggleHold() | Promise<void> | Toggle call hold |
| onHold({ holdStatus }) | Promise<boolean> | Set hold state explicitly |
| sendDTMF({ character }) | Promise<boolean> | Send DTMF tone (0-9, *, #) |
| getAudio() | Promise<any> | List available audio devices |
| setAudio({ portType }) | Promise<void> | Set audio output device |
| getCurrentAudio() | Promise<any> | Get current audio device |
Video Control
| Function | Returns | Description |
|----------|---------|-------------|
| toggleOmiVideo() | Promise<boolean> | Toggle video stream on/off |
| switchOmiCamera() | Promise<boolean> | Switch front/back camera |
| registerVideoEvent() | Promise<boolean> | Start receiving remote video frames |
| removeVideoEvent() | Promise<boolean> | Stop receiving remote video frames |
User & Info
| Function | Returns | Description |
|----------|---------|-------------|
| getCurrentUser() | Promise<any> | Get logged-in user details |
| getGuestUser() | Promise<any> | Get guest/remote user details |
| getUserInfo(phone) | Promise<any> | Look up user by phone number |
Getter Functions (v4.0.1+)
| Function | Returns | Description |
|----------|---------|-------------|
| getProjectId() | Promise<string\|null> | Current project ID |
| getAppId() | Promise<string\|null> | Current app ID. (v4.1.7+ Android) Sourced from OmiClient.getAppId() to match getOmiDevices() payload |
| getDeviceId() | Promise<string\|null> | Current device ID. (v4.1.7+ Android) Sourced from OmiClient.getDeviceId() to match getOmiDevices() payload |
| getFcmToken() | Promise<string\|null> | FCM push token |
| getSipInfo() | Promise<string\|null> | SIP info (user@realm) |
| getVoipToken() | Promise<string\|null> | VoIP token (iOS only) |
On-Premise Endpoint Configuration (v4.1.8+) — Native Only
For enterprise customers self-hosting OMI infrastructure. Override SDK default endpoints / SIP proxy / STUN / TURN with the customer's own hosts. Each field is optional — omit any field to keep the SDK default. Persisted natively across app relaunches.
There is no JavaScript API for this feature. Config must be set in AppDelegate (iOS) / MainApplication (Android), BEFORE React Native bootstraps. Setting from JS would race the SDK's first HTTP / SIP call (FCM token, push registration, VoIP push handler) and those requests would hit the default cloud endpoints. The native API persists the config so subsequent app launches are race-free.
iOS — AppDelegate.m
Add the call at the top of didFinishLaunchingWithOptions:, BEFORE the existing RN bootstrap and BEFORE any other Omi call:
#import <OmiKit/OmiKit.h>
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[OmiClient setOnPremiseInfoWithMobileSdkHost:@"omisdk.your-domain.com"
callEventHost:@"call-event.your-domain.com"
publicApiHost:@"public.your-domain.com"
pushInfoHost:@"push-info.your-domain.com"
app2AppHost:@"app-2-app.your-domain.com"
logUploadHost:@"log-upload.your-domain.com"
sipProxy:@"sig.your-domain.com:5222"
stunServer:@"stun.your-domain.com:3478"
turnServer:@"turn.your-domain.com:2222"
turnUsername:@"your-turn-user"
turnPassword:@"your-turn-pass"];
// ... existing RN bootstrap (RCTAppDelegate / RCTReactNativeFactory)
}To revert: [OmiClient clearOnPremiseInfo];
Android — MainApplication.kt
Add the call at the top of onCreate(), BEFORE SoLoader.init and RN init:
import vn.vihat.omicall.omisdk.OmiClient
class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
OmiClient.setOnPremiseInfo(
this,
mobileSdkHost = "omisdk.your-domain.com",
callEventHost = "call-event.your-domain.com",
publicApiHost = "public.your-domain.com",
pushInfoHost = "push-info.your-domain.com",
app2AppHost = "app-2-app.your-domain.com",
logUploadHost = "log-upload.your-domain.com",
sipProxy = "sig.your-domain.com:5222",
stunServer = "stun.your-domain.com:3478",
turnServer = "turn.your-domain.com:2222",
turnUsername = "your-turn-user",
turnPassword = "your-turn-pass",
)
SoLoader.init(this, false)
// ... existing RN bootstrap
}
}To revert: OmiClient.clearOnPremiseInfo(this).
Field Reference
HTTP host groups (SDK replaces scheme + host only; path + query preserved 100%):
| Field | Replaces |
|-------|----------|
| mobileSdkHost | omisdk-v1*.omicrm.com — devices, extensions, network info, ICE provider, rtp log |
| callEventHost | call-event-v2*.omicrm.com — call-action APIs |
| publicApiHost | public-v1*.omicrm.com — init call API |
| pushInfoHost | push-info-v2*.omicrm.com — has-answered |
| app2AppHost | app-2-app*.omicrm.com — agent/customer login |
| logUploadHost | elastic-v2*.omicrm.com — log upload |
SIP / Media ("host:port" format):
| Field | SDK default |
|-------|------------|
| sipProxy | 171.244.138.14:5222 |
| stunServer | stun.omicrm.com:3478 |
| turnServer | turn.omicrm.com:2222 |
| turnUsername | embedded credentials |
| turnPassword | embedded credentials |
Behavior Notes
- Config is persisted natively (iOS
NSUserDefaultskeyomicall/onpremise_config_v1, AndroidSharedPreferencesomicall_onpremise/config_v1) — no need to set on every app launch after the first. - Priority: HTTP → on-premise > SDK default. SIP / Media → on-premise > dynamic API provider > SDK default.
- Android DNS: when on-premise is active, the SDK bypasses custom public DNS (
8.8.8.8/1.1.1.1) and uses system DNS so internal hostnames resolve over the customer's private network / VPN. Applies to both OkHttp HTTP layer and PJSIP native. - Empty strings, null, and missing fields are treated identically as "keep SDK default" for that field.
- Requires native SDK: iOS
OmiKit ≥ 1.11.25, AndroidOMICore ≥ 2.7.4. - Backward compatible: clients that do not call
setOnPremiseInfosee byte-for-byte identical behavior to earlier versions.
Backend Device Registration Check (v4.1.7+)
Read-only diagnostics for verifying the local device record still exists on the OMI backend. Useful after app reinstall, account migration, or backend cleanup. The SDK never auto-logouts — your app decides the recovery action.
| Function | Returns | Description |
|----------|---------|-------------|
| getOmiDevices() | Promise<OmiDeviceInfo[]> | Fetch all devices registered on the OMI backend for the active SIP user. Empty array when not logged in / network error / parse error — never rejects |
| isCurrentDeviceRegistered() | Promise<boolean> | true if the local deviceId + appId is in the backend list. Returns false early when not logged in (no HTTP call) |
| needsReLogin() | Promise<boolean> | true when SIP user is set locally but the backend has no matching device (stale session — user must logout + login again) |
| findSipNumberByDeviceId(devices, deviceId) | string \| null | Pure JS helper. Returns the sipNumber for the given device ID in the array, or null if not found |
OmiDeviceInfo shape (camelCase, normalized at JS layer):
type OmiDeviceInfo = {
deviceId: string;
token: string;
deviceType: 'ios' | 'android' | string;
voipToken: string;
appId: string;
createdTime: string;
projectId: string;
sipNumber: string;
};Recommended usage:
import {
getOmiDevices,
needsReLogin,
findSipNumberByDeviceId,
getDeviceId,
logoutAndWait,
} from 'omikit-plugin';
// Or: verify the SIP account matches the one bound to this device
const devices = await getOmiDevices();
const myDeviceId = await getDeviceId();
const boundSip = findSipNumberByDeviceId(devices, myDeviceId ?? '');
if (boundSip !== expectedUsername) {
await logoutAndWait();
// Then re-login with correct credentials
}Notes:
- Each call performs a fresh HTTP request — no caching. Call once after login / on foreground, not in tight loops.
- Requires native SDK: iOS
OmiKit ≥ 1.11.19, AndroidOMICore ≥ 2.6.21.
Notification Control
| Function | Returns | Description |
|----------|---------|-------------|
| configPushNotification(data) | Promise<any> | Configure push notification settings |
| hideSystemNotificationSafely() | Promise<boolean> | Hide notification without unregistering |
| hideSystemNotificationOnly() | Promise<boolean> | Hide notification only |
| hideSystemNotificationAndUnregister(reason) | Promise<boolean> | Hide + unregister with reason |
Events
Use omiEmitter to listen for events emitted by the native SDK.
import { omiEmitter, OmiCallEvent } from 'omikit-plugin';Event Reference
| Event | Payload | Description |
|-------|---------|-------------|
| onCallStateChanged | { status, callerNumber, isVideo, incoming, codeEndCall } | Call lifecycle changes |
| onMuted | boolean | Microphone mute toggled |
| onSpeaker | boolean | Speaker toggled |
| onHold | boolean | Hold state changed |
| onRemoteVideoReady | — | Remote video stream is ready |
| onClickMissedCall | { callerNumber } | User tapped missed call notification |
| onSwitchboardAnswer | { data } | Switchboard answered |
| onCallQuality | { quality, stat } | Call quality metrics (see Quality & Diagnostics) |
| onAudioChange | { data } | Audio device changed |
| onRequestPermissionAndroid | { permissions } | Permission request needed (Android only) |
Usage Example
import { omiEmitter, OmiCallEvent, OmiCallState } from 'omikit-plugin';
useEffect(() => {
const subscriptions = [
// Call state changes
omiEmitter.addListener(OmiCallEvent.onCallStateChanged, (data) => {
console.log('State:', data.status, 'Caller:', data.callerNumber);
if (data.status === OmiCallState.incoming) {
// Navigate to incoming call screen
}
if (data.status === OmiCallState.confirmed) {
// Call connected
}
if (data.status === OmiCallState.disconnected) {
// Call ended, check data.codeEndCall for reason
}
}),
// Mute state
omiEmitter.addListener(OmiCallEvent.onMuted, (isMuted) => {
setMuted(isMuted);
}),
// Speaker state
omiEmitter.addListener(OmiCallEvent.onSpeaker, (isOn) => {
setSpeaker(isOn);
}),
// Missed call notification tapped
omiEmitter.addListener(OmiCallEvent.onClickMissedCall, (data) => {
// Navigate to call history or callback
}),
// Call quality & diagnostics
omiEmitter.addListener(OmiCallEvent.onCallQuality, ({ quality, stat }) => {
console.log('Quality level:', quality); // 0=Good, 1=Medium, 2=Bad
if (stat) {
console.log('MOS:', stat.mos, 'Jitter:', stat.jitter, 'Latency:', stat.latency);
}
}),
];
return () => subscriptions.forEach(sub => sub.remove());
}, []);Enums
OmiCallState
| Value | Name | Description |
|-------|------|-------------|
| 0 | unknown | Initial/unknown state |
| 1 | calling | Outgoing call initiated, waiting for response |
| 2 | incoming | Incoming call received |
| 3 | early | Early media (183 Session Progress) |
| 4 | connecting | 200 OK received, establishing media |
| 5 | confirmed | Call active, RTP media flowing |
| 6 | disconnected | Call ended |
| 7 | hold | Call on hold |
OmiStartCallStatus
| Value | Name | Description |
|-------|------|-------------|
| 0 | invalidUuid | Invalid user UUID |
| 1 | invalidPhoneNumber | Invalid phone number format |
| 2 | samePhoneNumber | Calling your own number |
| 3 | maxRetry | Max retry attempts exceeded |
| 4 | permissionDenied | General permission denied |
| 450 | permissionMicrophone | Microphone permission needed |
| 451 | permissionCamera | Camera permission needed |
| 452 | permissionOverlay | Overlay permission needed |
| 5 | couldNotFindEndpoint | SIP endpoint not found |
| 6 | accountRegisterFailed | SIP registration failed |
| 7 | startCallFailed | Call initiation failed |
| 8 | startCallSuccess | Call started successfully (Android) |
| 407 | startCallSuccessIOS | Call started successfully (iOS) |
| 9 | haveAnotherCall | Another call is in progress |
| 10 | accountTurnOffNumberInternal | Internal number has been deactivated |
| 11 | noNetwork | No network connection available |
OmiAudioType
| Value | Name | Description |
|-------|------|-------------|
| 0 | receiver | Phone earpiece |
| 1 | speaker | Speakerphone |
| 2 | bluetooth | Bluetooth device |
| 3 | headphones | Wired headphones |
End Call Status Codes (codeEndCall)
When a call ends (state = disconnected), the codeEndCall field in the onCallStateChanged event payload contains the status code indicating why the call ended.
Standard SIP Codes
| Code | Description |
|------|-------------|
| 200 | Normal call ending |
| 408 | Call timeout — no answer |
| 480 | Temporarily unavailable |
| 486 | Busy (or call rejected via rejectCall()) |
| 487 | Call cancelled before being answered |
| 500 | Server error |
| 503 | Server unavailable |
OMICALL Extended Codes
| Code | Description |
|------|-------------|
| 600 | Call declined |
| 601 | Call ended by customer |
| 602 | Call answered / ended by another agent |
| 603 | Call declined (via dropCall() — stops ringing on ALL devices) |
Business Rule Codes (PBX)
| Code | Description | |------|-------------| | 850 | Exceeded concurrent call limit | | 851 | Exceeded call limit | | 852 | No service plan assigned — contact provider | | 853 | Internal number has been deactivated | | 854 | Number is in DNC (Do Not Call) list | | 855 | Exceeded call limit for trial plan | | 856 | Exceeded minute limit for trial plan | | 857 | Number blocked in configuration | | 858 | Unknown or unconfigured number prefix | | 859 | No available number for Viettel direction — contact provider | | 860 | No available number for Vinaphone direction — contact provider | | 861 | No available number for Mobifone direction — contact provider | | 862 | Number prefix temporarily locked for Viettel | | 863 | Number prefix temporarily locked for Vinaphone | | 864 | Number prefix temporarily locked for Mobifone | | 865 | Advertising call outside allowed time window — try again later |
Common scenarios:
User hangs up normally → 200
Caller cancels before answer → 487
Callee rejects (this device) → 486 (rejectCall)
Callee rejects (all devices) → 603 (dropCall)
Callee busy on another call → 486
No answer / timeout → 408 or 480
Answered by another agent → 602
Exceeded concurrent call limit → 850
Number in DNC list → 854Usage:
omiEmitter.addListener(OmiCallEvent.onCallStateChanged, (data) => {
if (data.status === OmiCallState.disconnected) {
const code = data.codeEndCall;
if (code === 200) {
console.log('Call ended normally');
} else if (code === 487) {
console.log('Call was cancelled');
} else if (code === 602) {
console.log('Call was answered by another agent');
} else if (code >= 850 && code <= 865) {
console.log('Business rule error:', code);
// Show user-f