MyTM Liveness & KYC SDK for Flutter
Flutter SDK for liveness and KYC. Captures passport / ID images, reads MRZ, reads NFC chip data on supported devices, captures selfies for liveness and face‑match, and uploads everything securely to the backend for verification.
The SDK is intentionally thin. All sensitive verification logic — OCR, document liveness, face
liveness, face matching, and the final decision — runs on the backend, not in the SDK, so it
can't be bypassed or model‑extracted by a hostile client. The SDK also deliberately does not own
the camera: your app brings its own camera plugin and feeds a live preview into the SDK's
overlay widgets, then hands the captured JPEG bytes back to the SDK.
Contents
- Supported platforms
- Installation
- Platform setup (Android · iOS)
- API keys
- The full flow at a glance
- Step‑by‑step integration
- Complete example
- Capture modes & overlays
- Error handling
- Sandbox & testing
- Production checklist
- API surface
Supported platforms
| Platform | Min version | Camera | NFC |
|---|---|---|---|
| Android | API 24 (Android 7.0) | ✅ | ✅ requires android.permission.NFC |
| iOS | iOS 13.0 | ✅ | ✅ requires NFCReaderUsageDescription + entitlement |
Installation
dependencies:
mytm_liveness_sdk: ^1.0.0
# The SDK does not bundle a camera. Your app supplies the live preview:
camera: ^0.11.0
flutter pub get
During local development you can reference the SDK by path instead:
mytm_liveness_sdk: path: ../sullis-kyc-platform/mobile/flutter-sdk
Platform setup
Android
-
android/app/src/main/AndroidManifest.xml— camera + NFC:<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.NFC" /> <uses-feature android:name="android.hardware.camera" android:required="true" /> <uses-feature android:name="android.hardware.nfc" android:required="false" /> -
android/app/build.gradle—minSdkVersion 24or higher. -
ProGuard (release only) —
android/app/proguard-rules.pro:-keep class com.sullis.kyc.** { *; } -
Production NFC — pull in
jmrtd+scuba-sc-androidand implementSullisKycPlugin.readPassportInternal(see plugin source for the contract).
iOS
-
ios/Runner/Info.plist:<key>NSCameraUsageDescription</key> <string>Used to capture your passport and selfie for identity verification.</string> <key>NFCReaderUsageDescription</key> <string>Used to read your passport chip for identity verification.</string> <key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key> <array> <string>A0000002471001</string> </array> -
Entitlements — Xcode → Signing & Capabilities → + Capability → Near Field Communication Tag Reading.
-
Deployment target — iOS 13.0 minimum.
-
Production NFC — add
pod 'NFCPassportReader'to the plugin podspec and implementreadPassportInternalinSullisKycPlugin.swift.
API keys
Keys are minted per tenant and are environment‑prefixed:
- Sandbox:
sk_test_…→SullisEnvironment.sandbox - Live:
sk_live_…→SullisEnvironment.live
Never embed a live key in your shipped binary. Fetch a short‑lived key from your own backend at runtime, or have your backend mint a per‑session SDK token.
The full flow at a glance
initialize ─▶ createSession ─▶ captureDocument(…) ─▶ captureSelfie ─▶ submitVerification
│ (1+ sides, │
│ one "attempt") ▼
└──── optional: readAndSubmitNfc submit API response
(printed & returned)
A session represents one customer verification. Within a session you open one or more attempts; every artifact uploaded for a single attempt (each document side, the selfie, NFC) is verified together. You start a fresh attempt with the first document, then attach the remaining sides and the selfie to that same attempt.
Step‑by‑step integration
1. Initialize (once at startup)
import 'package:mytm_liveness_sdk/mytm_liveness_sdk.dart';
await SullisKycSdk.initialize(
baseUrl: 'https://api.sullis.example.com', // no trailing /api — the SDK appends /api/v1/…
apiKey: keyFromYourBackend,
environment: SullisEnvironment.sandbox,
);
2. Register callbacks (optional)
Callbacks mirror every stage of the flow, useful for logging or driving UI state:
SullisKycSdk.instance.setCallbacks(SullisCallbacks(
onStarted: () => log('▶ started'),
onDocumentCaptured: (d) => log('📄 uploaded ${d.sizeBytes} bytes'),
onSelfieCaptured: (s) => log('🤳 selfie ${s.sizeBytes} bytes'),
onProcessing: () => showSpinner(),
onVerified: (r) => onSuccess(r),
onRejected: (r) => onRejected(r.failureReason),
onManualReview: (caseId) => onPending(caseId),
onError: (e) => log('error: $e'),
));
3. Capture with the SDK camera overlay
The SDK ships overlay widgets (document guide, face oval, NFC prompt) but not a camera. Compose the
SDK's CaptureScreen with a live CameraPreview from the camera plugin, let the user frame the
target, grab the frame, and return its JPEG bytes. See
example/lib/screens/camera_capture_screen.dart
for a complete, copy‑pasteable capture screen. It pops with a Uint8List (or null if cancelled):
final Uint8List? bytes = await Navigator.of(context).push<Uint8List>(
MaterialPageRoute(
builder: (_) => CameraCaptureScreen(mode: CaptureMode.passportTd3, title: 'Passport'),
fullscreenDialog: true,
),
);
4. Create a session
final session = await SullisKycSdk.instance.createSession(
customerReference: 'CUSTOMER-${user.id}',
webhookUrl: 'https://api.yourapp.com/sullis/webhook', // optional
);
// createSession also refreshes per-tenant config (NFC on/off, branding, allowed doc types).
5. Upload documents — attempt grouping
Pass the bytes you captured in step 3. The first document of a verification must pass
startNewAttempt: true; additional sides pass false so they attach to the same attempt.
A passport is a single page:
await sdk.captureDocument(
sessionId: session.sessionId,
documentType: 'PASSPORT',
documentSide: 'FRONT',
imageBytes: passportBytes,
// startNewAttempt defaults to true
);
A national ID card is captured on both sides — same attempt:
await sdk.captureDocument(
sessionId: session.sessionId, documentType: 'NATIONAL_ID', documentSide: 'FRONT',
startNewAttempt: true, imageBytes: idFrontBytes,
);
await sdk.captureDocument(
sessionId: session.sessionId, documentType: 'NATIONAL_ID', documentSide: 'BACK',
startNewAttempt: false, imageBytes: idBackBytes, // attach to the same attempt
);
capturePassport(...)is a convenience wrapper overcaptureDocument(documentType: 'PASSPORT', documentSide: 'FRONT'). If you omitimageBytes, the SDK falls back to the native camera bridge.
6. Upload the selfie
The selfie attaches to the attempt opened in step 5:
await sdk.captureSelfie(sessionId: session.sessionId, imageBytes: selfieBytes);
7. (Optional) Read the NFC chip
Only when the tenant has NFC enabled and the document has a chip. The MRZ key (document number, DOB,
expiry — all YYMMDD for the dates) comes from the printed machine‑readable zone:
try {
await sdk.readAndSubmitNfc(
sessionId: session.sessionId,
documentNumber: mrz.documentNumber,
dateOfBirth: mrz.dateOfBirth, // YYMMDD
expiryDate: mrz.expiryDate, // YYMMDD
issuingCountry: 'USA', // 3-letter ISO
);
} on SullisNfcException {
// NFC unavailable / read failed — continue with image-only verification.
}
8. Submit for verification
submitVerification finalizes the attempt by calling the submit API, then prints the submit‑API
response via debugPrint('[Sullis] Submit API response: …') and returns it immediately as a
SullisVerificationResult:
final result = await sdk.submitVerification(
sessionId: session.sessionId,
timeout: const Duration(seconds: 60),
);
switch (result.outcome) {
case VerificationOutcome.verified: onSuccess(result); break;
case VerificationOutcome.declined: onDeclined(result.failureReason); break;
case VerificationOutcome.manualReview: onPending(result.manualReviewCaseId); break;
case VerificationOutcome.retryRequired: restartFromCapture(); break;
case VerificationOutcome.pending: // still processing server-side — poll the
// session later or wait for your webhook.
break;
}
Behavior note: as of this version
submitVerificationreturns as soon as the submit API responds and does not poll the session. If the submit response doesn't yet carry a terminal status,outcomeispending— use your webhook (or poll the session) for the authoritative final decision.
Complete example
A full, runnable flow — document‑kind selection, passport vs. ID front/back, selfie, and submit — is
in example/lib/screens/high_level_flow_screen.dart.
Run the example app:
cd example
flutter run --dart-define=SULLIS_API_KEY=sk_test_… --dart-define=SULLIS_BASE_URL=http://10.0.2.2:8080
Capture modes & overlays
CaptureMode selects the overlay and lens used by CaptureScreen:
CaptureMode |
Use for | Lens |
|---|---|---|
passportTd3 |
Passport photo page (TD‑3) | Back |
idCardTd1 |
National ID card (TD‑1) | Back |
drivingLicense |
Driving licence | Back |
cnicFront |
CNIC front | Back |
cnicBack |
CNIC back | Back |
face |
Selfie / liveness | Front |
Exported overlay widgets you can also use directly: PassportCameraOverlay, FaceCameraOverlay,
NfcCapturePrompt.
Error handling
Every error the SDK raises is a subclass of SullisException (a sealed type — exhaustive switch
works):
| Exception | When it fires | Suggested handling |
|---|---|---|
SullisAuthException |
API key invalid / expired / revoked | Re‑fetch key from your backend |
SullisNetworkException |
No connectivity, timeout, DNS failure | Offline UI; retry |
SullisApiException |
Server returned 4xx/5xx (has .statusCode, .code) |
Surface a per‑code message |
SullisCameraException |
Camera plugin failed | Ask the user to retry |
SullisPermissionException |
User denied camera/NFC | Deep‑link to Settings |
SullisNfcException |
NFC unavailable or read failed | Fall back to image‑only |
SullisSessionException |
Session/attempt missing, expired, or terminal | Create a new session |
SullisNotInitializedException |
API called before initialize() |
Call initialize() first |
try {
await sdk.submitVerification(sessionId: session.sessionId);
} on SullisApiException catch (e) {
showError('${e.code} (${e.statusCode}): ${e.message}');
} on SullisException catch (e) {
showError('${e.code}: ${e.message}');
}
Sandbox & testing
Sandbox talks to a real backend with relaxed AI thresholds. Against a local docker‑compose stack:
await SullisKycSdk.initialize(
baseUrl: 'http://10.0.2.2:8080', // Android emulator → host machine
apiKey: 'sk_test_<your-sandbox-key>',
environment: SullisEnvironment.sandbox,
);
For unit tests, inject bytes so no camera is needed:
await sdk.captureDocument(
sessionId: session.sessionId,
documentType:'PASSPORT',
imageBytes: testPassportJpegBytes, // skips the camera bridge
);
Production checklist
Before flipping to SullisEnvironment.live:
Live API URL configured (HTTPS only)API key fetched from your backend at runtime — not bundledWebhook URL registered in tenant settings (authoritative for the final outcome)Production NFC libraries wired in on both platformsCamera + NFC permissions tested on real devicesCrash reporting capturingSullisExceptioneventsUser‑facing error messages localizedRetry UX respecting the tenantmaxRetryCountsettingApp Store / Play Store privacy declarations cover biometric data
API surface
| Member | Purpose |
|---|---|
SullisKycSdk.initialize(...) |
One‑time setup (base URL, key, environment). |
SullisKycSdk.instance |
The singleton, after init. |
createSession(...) |
Start a customer session; refreshes tenant config. |
captureDocument(...) / capturePassport(...) |
Upload a document side; manages attempts. |
captureSelfie(...) |
Upload the selfie for the active attempt. |
readAndSubmitNfc(...) |
Read & submit passport chip data (if enabled). |
submitVerification(...) |
Submit the attempt; prints & returns the submit‑API response. |
setCallbacks(...) |
Register stage callbacks. |
dispose() |
Tear down; requires initialize() again afterwards. |
Result/model types: SullisSession, DocumentResult, SelfieResult,
SullisVerificationResult (outcome, riskScore, failureReason, manualReviewCaseId),
VerificationOutcome, SdkConfig, SullisEnvironment, SullisCallbacks.
Libraries
- mytm_liveness_sdk
- MyTM Liveness & KYC SDK for Flutter.