OTPLESS Flutter Headless SDK

The new Headless Authentication SDK offers faster performance, greater reliability, and enhanced security. For a smoother authentication and integration experience, we strongly recommend migrating by removing the old SDK and following the steps below.

Install OTPLESS SDK Dependency

pub package

Installation

dependencies: otpless_headless_flutter: ^<latest_version>
flutter pub get

Toolchain requirements (2.0.0+)

The underlying native SDKs (otpless-headless-sdk 2.0.1, OtplessBM/Core 3.0.1 as of plugin 3.0.0) pull in transitive dependencies that require newer toolchains than the pre-2.0 releases needed:

  • Android: Android Gradle Plugin 8.9.1+ and compileSdkVersion 36+. The Android SDK transitively depends on androidx.core:core:1.18.0, which enforces this minimum. Update android/settings.gradle and android/app/build.gradle in your consuming app accordingly.
  • iOS: deployment target 13.0+ (unchanged). CocoaPods with OtplessBM/Core 3.0.1 on the trunk. If you use OtplessChannelType.GOOGLE_SDK or FACEBOOK_SDK, add the matching subspec (OtplessBM/GoogleSupport, OtplessBM/FacebookSupport) to your ios/Podfile.
Plugin Android otpless-headless-sdk iOS OtplessBM/Core
3.0.0 2.0.1 3.0.1
2.0.0 0.9.0 2.3.2

Platform support matrix

Dart method Android iOS
initialize (sslPinning, loginUri — new in 2.1)
setResponseCallback
setDevLogging
start
startOnetap (OneTap discovery) — renamed from startBackground (new in 2.0)
commitResponse
isSdkReady
sendUserAuthEvent (new in 2.0)
setMfaEnabled
initSession / getActiveSession / logoutSession
startInBackground (silent regular auth) ❌ (no SDK equivalent)
isWhatsAppInstalledForAndroid ❌ returns false
initTrueCaller ❌ returns false (no iOS Truecaller SDK)
checkSimBindingStatus / clearSimBinding / setSimBindingEnabled ❌ (no SDK equivalent)
closeDialogIfOpen ❌ (no SDK equivalent)
Deep-link handling / Passkey / Facebook SDK register via manifest / no-op client wires up in AppDelegate / SceneDelegate (see below)

Initialize the SDK

Import

import 'package:otpless_headless_flutter/otpless_flutter.dart';
final _otplessHeadlessPlugin = Otpless();
@override
void initState() {
  super.initState();
  _otplessHeadlessPlugin.initialize("YOUR_APP_ID");
  _otplessHeadlessPlugin.setResponseCallback(onOtplessResponse);
}

initialize options (3.0.0+)

Future<void> initialize(
  String appId, {
  OtplessSslPinning sslPinning = OtplessSslPinning.disabled,
  String? loginUri,
});
Parameter Default Notes
sslPinning OtplessSslPinning.disabled Opt-in SSL certificate pinning of the OTPLESS backend on both platforms. See SSL pinning.
loginUri null (SDK derives otpless.<appid>://otpless) Deep-link URI the SDK returns to after OAuth channels. Forwarded to both native SDKs.

SSL pinning

Pinning is off by default. Turn it on at initialisation time:

_otplessHeadlessPlugin.initialize(
  "YOUR_APP_ID",
  sslPinning: OtplessSslPinning.enabled,
);

With pinning enabled the native SDKs (Android otpless-headless-sdk 2.0.1, iOS OtplessBM 3.0.1) validate the certificate chain of the OTPLESS backend (sigma.otpless.app) against a signed remote pin manifest before any authentication request is sent. If validation fails the SDK fails closed: no request leaves the device and your response callback receives

{
  "responseType": "FAILED",
  "statusCode": 5004,
  "response": { "errorCode": "5004", "errorMessage": "SSL pin validation failed" }
}

Common causes are a debugging proxy (Charles, Proxyman, mitmproxy) or a corporate TLS-inspecting gateway. Do not enable pinning in builds you intend to inspect with a proxy. Pinning cannot be toggled without calling initialize again.

Google Play Integrity (Android) needs no plugin configuration: it is internal to otpless-headless-sdk 2.0.1 and activates with the dependency bump. Your app must be distributed through Google Play and linked to a Play Console project for attestation to succeed; iOS has no equivalent.

Initiate Authentication

Phone Auth

Request

void startWithPhone(String phoneNumber) {
    final Map<String, dynamic> args = {
        "phone": "phoneNumber",
        "countryCode": "countryCode",
    };
    _otplessHeadlessPlugin.start(onOtplessResponse, args);
}

Verify

void verifyPhoneOtp(String phoneNumber, String otp) {
    final Map<String, dynamic> args = {
        "phone": "phoneNumber",
        "countryCode": "countryCode",
        "otp": "otp",
    };
    _otplessHeadlessPlugin.start(onOtplessResponse, args);
}

Response Handling

void onOtplessResponse(dynamic result) {
  _otplessHeadlessPlugin.commitResponse(result);

  final responseType = result['responseType'];

  switch (responseType) {
    case "SDK_READY":
      debugPrint("SDK is ready");
      break;

    case "FAILED":
      // Terminal SDK-level failure. See "SDK-level FAILED codes" below.
      final code = result["statusCode"];
      if (code == 5004) {
        debugPrint("SSL pin validation failed; nothing was sent to the backend");
      } else {
        debugPrint("SDK initialization failed: ${result["response"]}");
      }
      break;

    case "INITIATE":
      if (result["statusCode"] == 200) {
        debugPrint("Headless authentication initiated");
        final authType = result["response"]["authType"]; // This is the authentication type
        if (authType == "OTP") {
         // Take user to OTP verification screen
        } else if (authType == "SILENT_AUTH") {
          // Handle Silent Authentication initiation by showing 
          // loading status for SNA flow.
        }
      } else {
        // Handle initiation error. 
        // To handle initiation error response, please refer to the error handling section.
        if (Platform.isAndroid) {
          handleInitiateErrorAndroid(result["response"]);
        } else if (Platform.isIOS) {  
          handleInitiateErrorIOS(result["response"]);
        }
      }
      break;

    case "OTP_AUTO_READ":
      // OTP_AUTO_READ is triggered only in ANDROID devices for WhatsApp and SMS.
        final otp = result["response"]["otp"];
        debugPrint("OTP Received: $otp");
      break;

    case "VERIFY":
      final authType = result["response"]["authType"];
      if (authType == "SILENT_AUTH") {
        if (result["statusCode"] == 9106) {
            // Silent Authentication and all fallback authentication methods in SmartAuth have failed.
            //  The transaction cannot proceed further. 
            // Handle the scenario to gracefully exit the authentication flow 
        } else {
            // Silent Authentication failed. 
            // If SmartAuth is enabled, the INITIATE response 
            // will include the next available authentication method configured in the dashboard.
        }
      } else {
        // To handle verification failed response, please refer to the error handling section.
        if (Platform.isAndroid) {
          handleVerifyErrorAndroid(result["response"]);
        } else if (Platform.isIOS) {  
          handleVerifyErrorIOS(result["response"]);
        }
      }
      break;

    case "DELIVERY_STATUS":
        // This function is called when delivery is successful for your authType.
        final authType = result["response"]["authType"];
        // It is the authentication type (OTP, MAGICLINK, OTP_LINK) for which the delivery status is being sent
        final deliveryChannel = result["response"]["deliveryChannel"];
        // It is the delivery channel (SMS, WHATSAPP, etc) on which the authType has been delivered
        break;

    case "ONETAP":
      final token = result["response"]["token"];
      if (token != null) {
        debugPrint("OneTap Data: $token");
        // Process token and proceed
      }
      break;

    case "FALLBACK_TRIGGERED":
        // A fallback occurs when an OTP delivery attempt on one channel fails,  
        // and the system automatically retries via the subsequent channel selected on Otpless Dashboard.  
        // For example, if a merchant opts for SmartAuth with primary channal as WhatsApp and secondary channel as SMS,
        // in that case, if OTP delivery on WhatsApp fails, the system will automatically retry via SMS.
        // The response will contain the deliveryChannel to which the OTP has been sent.
        final newDeliveryChannel = result["response"]["deliveryChannel"];
        if (newDeliveryChannel != null) {
            // This is the deliveryChannel to which the OTP has been sent
        }
      break;

    default:
      debugPrint("Unknown response type: $responseType");
      break;
  }

}

SDK-level FAILED codes

These arrive with responseType: "FAILED"; statusCode and response.errorCode carry the same value.

statusCode errorMessage Meaning Platforms
5003 Failed to initialize the SDK initialize could not complete (network, bad appId, etc.). Retry initialize. Android, iOS
5004 SSL pin validation failed sslPinning was enabled and the backend certificate did not match the pinned set. The SDK fails closed; no auth request was sent. Check for a MITM proxy, or initialise with OtplessSslPinning.disabled. Android, iOS (2.1.0+)

Android manifest update

Add Network Security Config inside your android/app/src/main/AndroidManifest.xml file into your

android:networkSecurityConfig="@xml/otpless_network_security_config"

Ios info.plist update

Add the following block to your ios/Runner/info.plist file (Only required if you are using the SNA feature):

<dict>
	<key>NSAllowsArbitraryLoads</key>
	<true/>
	<key>NSExceptionDomains</key>
	<dict>
		<key>80.in.safr.sekuramobile.com</key>
		<dict>
			<key>NSIncludesSubdomains</key>
			<true/>
			<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
			<true/>
			<key>NSTemporaryExceptionMinimumTLSVersion</key>
			<string>TLSv1.1</string>
		</dict>
		<key>partnerapi.jio.com</key>
		<dict>
			<key>NSIncludesSubdomains</key>
			<true/>
			<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
			<true/>
			<key>NSTemporaryExceptionMinimumTLSVersion</key>
			<string>TLSv1.1</string>
		</dict>
	</dict>
</dict>


iOS AppDelegate / SceneDelegate integration

The plugin does not wrap iOS URL handlers, Facebook SDK registration, or WebAuthn. If your merchant flow requires them, add the following in your host iOS app.

In ios/Runner/AppDelegate.swift:

import OtplessBM

override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    if Otpless.shared.isOtplessDeeplink(url: url) {
        Task { await Otpless.shared.handleDeeplink(url) }
        return true
    }
    return super.application(app, open: url, options: options)
}

For SceneDelegate apps, add to SceneDelegate.swift:

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    for context in URLContexts where Otpless.shared.isOtplessDeeplink(url: context.url) {
        Task { await Otpless.shared.handleDeeplink(context.url) }
    }
}

Facebook SDK (only if using FACEBOOK_SDK channel)

Add OtplessBM/FacebookSupport subspec to ios/Podfile. Then wire it up in AppDelegate.swift:

override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    Otpless.shared.registerFBApp(application, didFinishLaunchingWithOptions: launchOptions)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    Otpless.shared.registerFBApp(app, open: url, options: options)
    return super.application(app, open: url, options: options)
}

Passkey / WebAuthn

Otpless.shared.authorizeViaPasskey(withRequest:windowScene:) is not exposed through this Flutter plugin. If your flow needs it, call it from Swift with a resolved UIWindowScene.

Migration from 2.0 to 3.0

One breaking change: initialize(..., timeout:) is removed (it was never applied natively) — drop timeout: from your call site. initialize gains two optional named parameters (sslPinning, loginUri). Your response handler may now receive statusCode: 5004 when you opt in to pinning (see SSL pinning).

Migration from 1.x to 2.0

Breaking changes

  • startBackground(callback, config) is renamed to startOnetap(callback, config). The old name implied a background OTP flow, but the method actually presents the OneTap / verified-contact sheet. Replace every call site.
  • iOS parity for startOnetap and sendUserAuthEvent. These now execute on both platforms instead of silently returning false on iOS. If your app relied on iOS being a no-op, add a Platform.isAndroid guard on your side.
  • New responseType values may arrive in the callback: AUTH_TERMINATED, MFA_FACTOR_COMPLETED, AUTO_FLOW_ACTION (Android only). Callers that switch exhaustively on responseType must add cases.
  • OtplessAuthConfig constructor gains an optional deviceFingerprintMode parameter (defaults to DeviceFingerprintMode.none). Backwards compatible for positional callers.

New features

  • setMfaEnabled(bool) — enable MFA. Watch for MFA_FACTOR_COMPLETED in your response handler.
  • initSession(appId) / getActiveSession() / logoutSession() — JWT-based session persistence.
  • startInBackground(callback, requestMap) — Android-only silent variant of start() that suppresses OTP_AUTO_READ intermediates.
  • Android-only: checkSimBindingStatus(), clearSimBinding(), setSimBindingEnabled(bool), closeDialogIfOpen().
  • Extra keys accepted on the start() / startInBackground() request map: tid, code, extras (Map<String, String>), requestId, deviceFingerprintMode.

Bug fix

  • sendUserAuthEvent(...): the providerInfo optional parameter was previously dropped due to an inverted null check on the plugin side. Fixed in 2.0.0; downstream analytics receive it now.

Note

For complete documentation and other login feature explore, follow the following guide here: installation guide here

Author

OTPLESS, developer@otpless.com