deepidsdk_flutter
Device fraud detection and SIM binding for Flutter, on Android and iOS.
Enrolls the device with a fingerprint-backed deepId, cryptographically binds
the user's phone number to it, and attests device posture on demand for
high-risk operations.
Requirements
| Minimum | |
|---|---|
| Android | API 29 |
| iOS | 15.0 |
| Flutter | 3.10 |
| Dart | 3.0 |
Installation
dependencies:
deepidsdk_flutter: ^2.4.0
permission_handler: ^11.0.0 # Android runtime permissions
The native SDKs are downloaded by your build, pinned to the versions this
plugin was built against. iOS needs no setup. Android needs a
read:packages token from DeepID, added once to ~/.gradle/gradle.properties:
gpr.user=<your GitHub username>
gpr.key=<token>
GITHUB_USERNAME / GITHUB_TOKEN are also accepted, as Gradle properties or
environment variables, for CI.
If your settings.gradle sets FAIL_ON_PROJECT_REPOS
The plugin normally injects its repository into your build. That mode rejects it, so declare it yourself:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/surepassio/deepidsdk-android")
credentials {
username = providers.gradleProperty("gpr.user").get()
password = providers.gradleProperty("gpr.key").get()
}
content { includeGroup("com.deepidsdk") }
}
}
}
Quick start
import 'dart:io';
import 'package:deepidsdk_flutter/deepidsdk_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
// 1. Initialize once at startup. Returns immediately; enrollment runs in the
// background and onEnrollment fires when deepId + sessionId are ready.
await DeepId.initialize(
appKey: 'YOUR_APP_KEY',
appSecret: 'YOUR_APP_SECRET',
onEnrollment: (result) => setState(() => _enrolled = true),
onEnrollmentError: (e) => showError(e.message),
);
// 2. Bind the SIM — only after onEnrollment has fired.
Future<void> bindSim() async {
if (Platform.isAndroid) {
final s = await [Permission.phone, Permission.sms].request();
if (!s[Permission.phone]!.isGranted || !s[Permission.sms]!.isGranted) return;
}
try {
final result = await DeepId.startSimBinding(phoneNumber: '+919876543210');
print('Bound ${result.mobile} to ${result.deepId}');
} on DeepIdException catch (e) {
if (e.code != DeepIdErrorCode.userCancelled) showError(e.message);
}
}
Request the Android permissions immediately before startSimBinding(), not at
launch — the system ties the rationale dialog to the action that needs it.
phoneNumber is optional; without it the native sheet lists the device's SIMs
for the user to choose from.
Platform setup
Android
Set minSdk 29 in android/app/build.gradle. The SDK's permissions are merged
into your manifest automatically; nothing to add. Be aware of what lands in your
APK, since some entries affect Play Console review:
| Kind | Permissions |
|---|---|
| Runtime, requested before SIM binding | READ_PHONE_STATE, READ_PHONE_NUMBERS, SEND_SMS, READ_SMS |
| Install-time | INTERNET, ACCESS_NETWORK_STATE, ACCESS_WIFI_STATE |
| Policy-restricted, merged silently | PACKAGE_USAGE_STATS, SYSTEM_ALERT_WINDOW, REQUEST_INSTALL_PACKAGES, QUERY_ALL_PACKAGES |
READ_SMS backs the UPI sent-box check. It shares a permission group with
SEND_SMS, so Permission.sms grants both behind one dialog — but declare it
in your Play Console SMS form.
iOS
Set platform :ios, '15.0' in your Podfile. The SDK links
LocalAuthentication, AVFoundation, CoreLocation and CoreMotion for
availability checks only — no prompt is ever shown — but App Review's static
analysis requires the usage strings regardless. Add to ios/Runner/Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>DeepID Protect checks for biometric hardware availability during enrollment.</string>
<key>NSCameraUsageDescription</key>
<string>DeepID Protect queries camera hardware capabilities to build a device fingerprint. No image or video is captured.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>DeepID Protect checks location-service availability as a device-integrity signal.</string>
<key>NSMotionUsageDescription</key>
<string>DeepID Protect inspects motion-sensor availability as a device-integrity signal. No sensor data is recorded.</string>
Do not add Contacts, Photos, Microphone or Tracking descriptions; the SDK links
none of those frameworks. It ships its own PrivacyInfo.xcprivacy.
API
Everything is static on DeepId. Full reference, with the security notes each
method carries, is in the API docs.
| Method | Purpose |
|---|---|
initialize() |
Construct the SDK and start background enrollment. Idempotent. |
onEnrollment() |
Attach an enrollment callback from anywhere in the tree. |
startSimBinding() |
Present the native SIM binding flow; resolves with the verified number. |
verifySecurityPolicy() |
Re-attest device posture now for one operation. Never throws. |
prewarmAttestation() |
Pre-fetch the attestation challenge so the call at button press is one round trip. |
getFreshDeviceIntelligence() |
Collect a live device-intelligence object. |
logout() |
Clear the session so the next initialize() enrolls afresh. |
isInitialized, isAttestationSupported, deepId, sessionId, deviceIntelligence |
State accessors. |
startSimBinding() takes two optional mid-flow observers: onSimBindingInit
(the /sim-binding/init response, per attempt) and onSmsSentCheck (the UPI
sent-box verdict, Android only).
Two rules that are not optional
1. An attestation verdict is advisory. Your backend is the security boundary.
A compromised device can patch any client branch that checks Allowed. Send
Allowed.attestationId to your server and have it redeem the attestation
against DeepID before honouring the operation; if that does not return allow,
the operation does not happen. Treat Unavailable as Blocked for anything
critical.
switch (await DeepId.verifySecurityPolicy(action: 'payment.initiate', reference: orderId)) {
case Allowed(:final attestationId): await backend.pay(orderId, attestationId);
case Blocked(:final displayMessage): showError(displayMessage);
case Challenge(:final attestationId): await stepUp(attestationId);
case Unavailable(): showError('Could not verify device security.');
}
2. SimBindingInitResponse.bindingHash and .smsContent are the live token.
They are exposed for server-side correlation. Do not log them, persist them, or
send the SMS yourself — a token sent from anywhere but the bound SIM defeats
what SIM binding proves. Log clientId instead; toString() already redacts
both fields.
Errors
Every failure is a DeepIdException with a typed code — no other exception
type crosses the plugin boundary, and verifySecurityPolicy() never throws at
all. Callback failures you do not handle go to FlutterError.reportError, so
they surface in crash reporting rather than vanishing.
Two codes deserve their own branch:
| Code | Meaning | Do |
|---|---|---|
userCancelled |
The user dismissed the sheet. | Nothing — a normal path. |
bindingAbandoned |
The user left the app mid-binding. UPI rules make this a failed binding, not a dismissal. | Explain, offer a retry. |
Others: notInitialized, invalidAppKey, invalidAppSecret, noContext,
noViewController, initFailed, enrollmentFailed, enrollmentTimeout,
enrollmentNotComplete, simBindingFailed, deviceIntelligenceFailed,
alreadyInProgress, loggedOut, unsupportedPlatform, malformedResponse,
unknown — each documented on DeepIdErrorCode.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
DeepID SDK credentials are missing (Gradle) |
No gpr.user / gpr.key found — see Installation. |
401 resolving com.deepidsdk:deepidsdk-android |
Token lacks read:packages. A gh auth token does not carry it; use a PAT issued with that scope. |
Framework 'DeepIdSDK' not found (iOS) |
The download was undone. rm -rf ios/Pods ios/Podfile.lock && pod install. |
App exits ~1 s after initialize() on a tethered iPhone |
The release SDK exits when a debugger is attached; flutter run attaches one. Install the build and launch from the home screen. End users are unaffected. |
bindingAbandoned the moment the app is left |
Working as designed under UPI rules. Complete the flow without switching apps or locking the screen. |
NO_CONTEXT (Android) |
initialize() ran before an Activity existed. Call it from initState or a post-frame callback. |
ALREADY_IN_PROGRESS |
A previous startSimBinding() is still pending. Disable the button while in flight. |
Full guide, including checksum failures, proxy allow-lists and OEM-specific SMS behaviour: doc/troubleshooting.md.
Further reading
- API reference — every method, type and error code
- Platform behaviour — how Android and iOS differ
in permissions, UI, the
mobilefield, and what counts as leaving the app - Changelog
Support
Libraries
- deepidsdk_flutter
- DeepID SDK Flutter Plugin