usesmileid 12.1.1
usesmileid: ^12.1.1 copied to clipboard
Official Smile ID Flutter SDK for identity verification: selfie capture, liveness checks, and document verification powered by on-device ML.
Smile ID Flutter SDK #
The UseSmileID Flutter SDK lets you embed identity verification flows into your Flutter app using a type-safe DSL builder. Compose screens like LEGO bricks — no predefined product flows, no subclassing.
SDK Size #
Sizes are measured on every push to main and updated automatically by CI.
| Package | Download Size | Install Size |
|---|---|---|
usesmileid |
— | — |
usesmileid_bridge |
— | — |
usesmileid_mlkit_face |
— | — |
usesmileid_mlkit_document |
— | — |
usesmileid_huawei_face |
— | — |
usesmileid_huawei_document |
— | — |
usesmileid_vision_face |
— | — |
usesmileid_vision_document |
— | — |
| Sample app (Android) | — | — |
| Sample app (iOS) | — | — |
Requirements #
- Flutter 3.44+
- Dart 3.12+
- Android API 24+ (
minSdk);compileSdk36+ (Flutter's default) & Android Gradle Plugin 9.1+ - iOS 15.0+
Installation #
The SDK ships as a set of pub packages: the usesmileid entry point plus one ML analyzer
provider package per capture type and platform. Add the providers for your target
platforms to your pubspec.yaml:
dependencies:
usesmileid: ^12.1.1
# ML analyzer providers — include the capture types your flows use
# Android (GMS)
usesmileid_mlkit_face: ^12.1.1
usesmileid_mlkit_document: ^12.1.1
# iOS
usesmileid_vision_face: ^12.1.1
usesmileid_vision_document: ^12.1.1
(On no-GMS Android targets, swap the mlkit packages for usesmileid_huawei_{face,document}.)
Each provider is a platform-scoped Flutter plugin, so declaring them all together is safe —
only the packages matching the running platform register native code. The shared
usesmileid_bridge and usesmileid_platform_interface packages come in transitively
through usesmileid; you don't declare them yourself.
Then run:
flutter pub get
Android: Android Gradle Plugin 9 #
The SDK's Android plugins use AGP's built-in Kotlin, so your app needs
Android Gradle Plugin 9.1+ with a Gradle 9 wrapper. Set the version in your app's
android/settings.gradle(.kts):
id("com.android.application") version "9.1.1" apply false
If your app is on AGP 8, the build fails with:
Dependency 'androidx.core:core:1.19.0' requires Android Gradle plugin 9.1.0 or higher.
Android Studio's AGP Upgrade Assistant handles the upgrade. Apps that still apply the Kotlin Gradle Plugin should also follow Flutter's built-in Kotlin migration guide.
Keep the Kotlin Gradle Plugin declared in your app's android/settings.gradle(.kts), even
after that migration removes it from android/app/build.gradle(.kts). Flutter applies it to
plugin modules from there, and without it their Kotlin does not compile:
id("org.jetbrains.kotlin.android") version "2.4.0" apply false
Quick Start #
Place UseSmileIDBuilder anywhere in your widget tree. It is a Widget — use it exactly like Column or Stack.
import 'package:flutter/material.dart';
import 'package:usesmileid/usesmileid.dart';
class VerificationScreen extends StatelessWidget {
const VerificationScreen({super.key});
@override
Widget build(BuildContext context) {
return UseSmileIDBuilder(
builder: (smile) {
smile.onResult = (result) {
switch (result) {
case UseSmileIDSuccess(:final value):
print('Job: ${value.jobId}');
case UseSmileIDFailure(:final error):
print('Error: $error');
case UseSmileIDCancelled():
print('User exited before finishing');
}
};
smile.userDetails = const UserDetails(
givenNames: 'Ada',
lastName: 'Lovelace',
email: 'ada@example.com',
);
smile.network((n) => n.config((c) {
c.jobType = JobType.smartSelfieEnrollment;
c.token = 'your-v3-token';
c.partnerConfig((p) => p.partnerId = 'your-partner-id');
}));
smile.ml((ml) => ml.analyzers((a) => a.forCaptureType(CaptureType.selfie)));
smile.screens((screens) {
screens.consent((consent) {
consent.partnerName = 'Acme Corp';
consent.partnerIcon = const Icon(Icons.business);
consent.partnerPrivacyPolicyUrl = 'https://acme.com/privacy';
});
screens.instructions();
screens.capture((c) => c.captureType = CaptureType.selfie);
screens.preview();
screens.processing();
});
},
);
}
}
Every block above is required for a flow that submits: the SDK validates the whole
configuration before the first screen paints, and a flow that fails validation renders an
empty surface and reports a BuilderValidationException through onResult. Turn on
smile.config((c) => c.enableDebugMode = true) during development to see which rules failed.
Selfie capture needs a face analyzer provider from Installation; with the
providers declared, the running platform's default factory is used automatically — override
it per capture type in ml.
Full Builder Reference #
config — Global settings #
smile.config((config) {
config.enableDebugMode = false;
config.allowOfflineMode = false;
config.enableCrashReporting = true;
config.showAttribution = true;
});
| Property | Type | Default | Description |
|---|---|---|---|
enableDebugMode |
bool |
false |
Shows a validation error overlay on build failure |
allowOfflineMode |
bool |
false |
Allows the flow to run without a network connection |
enableCrashReporting |
bool |
true |
Enables Sentry crash reporting for the SDK. Set to false to opt out |
showAttribution |
bool |
true |
Shows the "Powered by Smile ID" mark on every screen that carries it. Set to false to hide it across the whole journey. Document capture never shows the mark |
theme — UI customisation #
smile.theme((theme) {
theme.primaryColor = theme.color(light: const Color(0xFF1A73E8), dark: const Color(0xFF4DA3FF));
theme.primaryForeground = theme.color(light: const Color(0xFFFFFFFF), dark: const Color(0xFF000000));
theme.secondaryColor = theme.color(light: const Color(0xFF5F6368), dark: const Color(0xFF9AA0A6));
theme.accentColor = theme.color(light: const Color(0xFF34A853), dark: const Color(0xFF81C995));
theme.fontFamily = 'Inter';
theme.buttonShape = 12.0;
theme.cardShape = 16.0;
});
| Property | Type | Default | Description |
|---|---|---|---|
primaryColor |
AdaptiveColor |
SDK default | Main action colour (buttons, highlights) |
primaryForeground |
AdaptiveColor |
SDK default | Text/icons on primary colour |
secondaryColor |
AdaptiveColor |
SDK default | Secondary surface colour |
accentColor |
AdaptiveColor |
SDK default | Accent highlights |
fontFamily |
String? |
null (system font) |
Custom font family name |
buttonShape |
double |
32.0 |
Corner radius for buttons |
cardShape |
double |
16.0 |
Corner radius for cards |
color() is a helper on ThemeConfigBuilder that creates an AdaptiveColor:
theme.primaryColor = theme.color(light: const Color(0xFF1A73E8), dark: const Color(0xFF4DA3FF));
Localization #
The SDK ships with English defaults for all 85 si_* keys (canonical list in lib/l10n/intl_en.arb). Partners localise by dropping a lib/l10n/intl_<lang>.arb file in their own app and declaring it in pubspec assets — the SDK auto-loads it at flow start based on the device locale.
# partner_app/pubspec.yaml
flutter:
assets:
- lib/l10n/intl_fr.arb
// partner_app/lib/l10n/intl_fr.arb
{
"@@locale": "fr",
"si_consent_title": "{partnerName} souhaite vérifier votre identité avec Smile ID.",
"si_consent_allow": "Autoriser",
"si_consent_deny": "Refuser"
}
Parameterized strings use named-token {name} placeholders. See docs/Localization.md for the full integrator guide.
network — API configuration #
All fields are optional. Omit the network block entirely to use SDK defaults.
smile.network((n) {
n.config((c) {
c.jobType = JobType.documentVerification;
c.token = 'your-v3-token';
// Optional: refresh on 401 mid-flow. Invoked when the server returns
// 401; return a fresh token and the SDK retries once.
c.onTokenExpired = (previous) async => fetchFreshToken();
c.partnerConfig((p) {
p.partnerId = 'your-partner-id';
p.callbackUrl = 'https://partner.example.com/job-callback';
p.useSandbox = false;
p.partnerParams = const {'flow_tag': 'kyc-v2'};
});
c.logging((l) {
l.enabled = true;
l.level = LogLevel.basic;
});
});
n.timeouts((t) {
t.connect = const Duration(seconds: 30);
t.read = const Duration(seconds: 60);
t.write = const Duration(seconds: 60);
t.call = const Duration(seconds: 120);
});
n.cache((c) {
c.enabled = true;
c.maxSize = 100 * 1024 * 1024; // 100 MB
});
n.interceptors((i) {
i.add(ChuckerDioInterceptor());
});
});
config block
| Property | Type | Default | Description |
|---|---|---|---|
jobType |
JobType |
JobType.unknown |
Job type sent with every request |
token |
String |
'' |
Short-lived v3 auth token. Exchange your long-lived API key for one from your own backend (POST /v3/token) and supply it here. The SDK stamps it on every authed request. |
onTokenExpired |
Future<String> Function(String previousToken)? |
null |
Optional refresh callback. Invoked on a 401 Unauthorized response on an authed request. The SDK calls it with the token it was using, expects a fresh token back, and retries the failed request once. Concurrent 401s collapse to a single callback invocation. If the callback throws or the retry also returns 401, the original 401 surfaces unchanged. |
partnerConfig block
| Property | Type | Default | Description |
|---|---|---|---|
partnerId |
String |
'' |
Your Smile ID partner ID |
callbackUrl |
String |
'' |
Partner webhook URL. Sent as the callback-url HTTP header on every request and as the callback_url multipart form part on Enhanced KYC / Biometric KYC submissions when non-empty. |
useSandbox |
bool |
false |
Route requests to the sandbox environment |
partnerParams |
Map<String, String>? |
null |
Partner-defined key/value pairs attached to the job and echoed back on the result. Forwarded as the partner_params form part (JSON) on Enhanced KYC / Biometric KYC submissions. |
logging block
| Property | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
true |
Enable network logging |
level |
LogLevel |
LogLevel.basic |
none / basic / headers / body |
Note:
LogLevel.bodyonly prints response and error-response bodies, and only in debug builds (kDebugMode = true). In release builds the body lines are suppressed to prevent KYC PII from appearing in device logs; header and status-line logging is unaffected.
timeouts block
| Property | Default |
|---|---|
connect |
Duration(seconds: 60) |
read |
Duration(seconds: 60) |
write |
Duration(seconds: 60) |
call |
Duration(seconds: 120) |
cache block
| Property | Type | Default |
|---|---|---|
enabled |
bool |
true |
maxSize |
int |
50 * 1024 * 1024 (50 MB) |
ml — Machine learning analyzers #
Detection runs in the native analyzer provider packages (see Installation). Pick the ones that match your platform / device fleet:
| Platform | Face | Document | Native dep |
|---|---|---|---|
| iOS | usesmileid_vision_face |
usesmileid_vision_document |
Apple Vision (SPM) |
| Android (GMS) | usesmileid_mlkit_face |
usesmileid_mlkit_document |
Google ML Kit (Maven) |
| Android (no GMS) | usesmileid_huawei_face |
usesmileid_huawei_document |
Huawei HMS (Maven) |
Each face package exports a FaceAnalyzerFactory; register the running platform's
factory per capture type:
import 'package:flutter/foundation.dart';
import 'package:usesmileid_mlkit_face/usesmileid_mlkit_face.dart';
import 'package:usesmileid_vision_face/usesmileid_vision_face.dart';
FaceAnalyzerFactory _selfieAnalyzerFactory() {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return const MlKitFaceAnalyzerFactory();
case TargetPlatform.iOS:
return const VisionFaceAnalyzerFactory();
default:
throw UnsupportedError('UseSmileID supports Android and iOS only.');
}
}
smile.ml((ml) {
ml.analyzers((a) {
a.forCaptureType(CaptureType.selfie, (c) {
c.add(_selfieAnalyzerFactory());
});
});
});
If you skip add(...), the loaded face plugin's own default factory is used. The
document packages have no Dart-side API — declaring the dependency wires the native
document analyzer automatically through Flutter's plugin registration.
screens — Flow composition #
Call screen functions inside the screens block. The flow navigates through them in the order they are declared.
smile.screens((screens) {
screens.consent((consent) {
consent.partnerName = 'Acme Corp';
consent.partnerIcon = const Icon(Icons.business);
consent.partnerPrivacyPolicyUrl = 'https://acme.com/privacy';
consent.onConsentGranted = (info) => print('Granted: $info');
});
screens.instructions((_) {});
screens.capture((capture) {
capture.captureType = CaptureType.selfie;
capture.selfie((selfie) {
selfie.allowAgentMode = false;
selfie.enableEnhancedLiveness = true;
});
});
screens.capture((capture) {
capture.captureType = CaptureType.document;
capture.document((doc) {
doc.documentType = const GenericDocument();
doc.captureMode = const AutoCaptureWithManualFallback(
activateManualAfter: Duration(seconds: 10),
);
doc.allowGalleryUpload = false;
doc.captureBothSides = true;
doc.allowSkipBack = false;
doc.knownIdAspectRatio = 1.586; // CR-80 card ratio (optional)
});
});
screens.preview((preview) {
preview.allowRetake = true;
});
screens.processing((processing) {
processing.showProgressPercentage = true;
});
});
Screen types
| Screen | Builder method | Key properties |
|---|---|---|
| Consent | screens.consent() |
partnerName, partnerIcon, partnerPrivacyPolicyUrl, onConsentGranted, allowButton, denyButton (partnerIcon and partnerPrivacyPolicyUrl are required) |
| Instructions | screens.instructions() |
showHeroOval, continueButton |
| Selfie capture | screens.capture((c) => c.captureType = CaptureType.selfie) |
allowAgentMode, enableEnhancedLiveness |
| Document capture | screens.capture((c) => c.captureType = CaptureType.document) |
documentType (required), captureMode, allowGalleryUpload, captureBothSides, allowSkipBack, knownIdAspectRatio |
| Preview | screens.preview() |
allowRetake |
| Processing | screens.processing() |
showProgressPercentage |
Attribution is a journey-wide setting: config.showAttribution = false hides the
"Powered by Smile ID" mark on every screen that carries it, the selfie capture surface and the
back-of-ID interstitial included. There is no per-screen switch. Document capture carries no
mark by design. See ../docs/Theming.md.
Capture screen rendering
The selfie/document capture screen draws the camera preview and the face oval edge-to-edge — the preview spans the full screen, including the status bar, and the status bar is rendered transparent over it. This keeps the oval reference frame identical across platforms (it matches the iOS capture screen, which is the agreed reference). The on-screen chrome (back button, guidance text, Start Capture button, attribution) stays inside the safe area.
On Android, the status bar only fully clears under the preview when the host app runs its window in edge-to-edge layout mode. The SDK does not enable edge-to-edge globally (that would mutate the partner app's window), so if a residual status-bar inset remains, configure the host app for edge-to-edge.
Enhanced Smart Selfie (enableEnhancedLiveness)
Enhanced Smart Selfie liveness — including the head-turn challenge, subject
continuity into the head-turn phase, the face-lost / identity-change reset, and
the whole-capture timeout — runs entirely in the native session. The Flutter
SDK observes the resulting UseSmileIDScanState (unsatisfied triggers a
clean restart that drops any accumulated capture paths; timeout restores the
Start Capture button for an in-place retry) and maps it to UI. There is no
liveness business logic in Dart; turning the flag on is the only step.
Top-level builder callbacks #
| Property | Type | Description |
|---|---|---|
onResult |
void Function(UseSmileIDResult<JobSubmissionResponse>)? |
Called once when the flow finishes (success or failure) |
onAnalyticsEvent |
void Function(UseSmileIDAnalyticsEvent)? |
Optional. Called for each analytics event during the flow |
Handling results #
Assign smile.onResult inside the builder callback. It is called once when the flow finishes.
UseSmileIDBuilder(
builder: (smile) {
smile.onResult = (result) {
switch (result) {
case UseSmileIDSuccess(:final value):
print('Job ${value.jobId} status=${value.status}');
case UseSmileIDFailure(:final error):
print('Failed: $error');
case UseSmileIDCancelled():
print('User exited before finishing');
}
};
// ... configure flow
},
)
Payload #
UseSmileIDResult<JobSubmissionResponse> is a sealed class with three branches, so the switch
must handle all of them: UseSmileIDSuccess, UseSmileIDFailure, and UseSmileIDCancelled
(the user left the flow before it finished — no job was submitted, and there is no error to
report). The success branch carries the server's submission acknowledgement:
| Field | Type | Description |
|---|---|---|
jobId |
String |
Server-issued job identifier |
userId |
String |
Server-issued user identifier |
status |
String |
Submission status (e.g. "submitted") |
message |
String |
Human-readable status message |
createdAt |
String? |
ISO 8601 timestamp at which the server accepted the job |
The result does not echo back partner-supplied inputs (captured frames, identity fields, consent). Keep references at the call site if you need them after the flow.
Analytics #
Assign smile.onAnalyticsEvent inside the builder callback to receive a stream of events fired at key moments in the flow. Each event carries a flat Map<String, String> that you can forward directly to any analytics backend.
UseSmileIDBuilder(
builder: (smile) {
smile.onAnalyticsEvent = (event) {
// Forward to Firebase, Mixpanel, or your own backend
FirebaseAnalytics.instance.logEvent(
name: event.type,
parameters: event.extras,
);
};
// ... configure flow
},
)
Every event automatically includes session_id and timestamp (epoch milliseconds) so you can correlate events across a single flow run without any extra bookkeeping.
Event reference #
| Event | When fired | Key extras |
|---|---|---|
flow_started |
Flow initialises | job_type |
screen_viewed |
Each screen becomes active | screen_name |
consent_captured |
User grants consent | decision: "granted" |
selfie_captured |
Selfie + liveness captured | liveness_image_count |
document_captured |
Document image captured | document_side: "front" or "both" |
retake_requested |
User navigates back to redo a step | — |
job_submitted |
API submission begins | job_type, attempt |
flow_completed |
Flow finishes | result: "success" or "failure", job_id (success), error_message (failure) |
onAnalyticsEvent is null by default — omit it and no events are delivered.
Pre-supplied consent #
If the user already consented in a previous session, skip the consent screen by supplying consentInformation directly on the flow builder:
UseSmileIDBuilder(
builder: (smile) {
final consentInfo = ConsentInformation(/* ... */);
smile.consentInformation = consentInfo;
smile.screens((screens) {
// do not add a consent screen when consentInformation is set — the builder will reject it
screens.instructions();
screens.capture((c) => c.captureType = CaptureType.selfie);
});
},
)
Full example — Document verification #
import 'package:flutter/material.dart';
import 'package:usesmileid/usesmileid.dart';
class DocumentVerificationScreen extends StatelessWidget {
const DocumentVerificationScreen({super.key});
@override
Widget build(BuildContext context) {
return UseSmileIDBuilder(
builder: (smile) {
smile.onResult = (result) {
switch (result) {
case UseSmileIDSuccess(:final value):
print('Done: ${value.jobId}');
case UseSmileIDFailure(:final error):
print('Failed: $error');
case UseSmileIDCancelled():
print('User exited before finishing');
}
};
smile.onAnalyticsEvent = (event) {
print('[SmileID] ${event.type} — session: ${event.extras['session_id']} ${event.extras}');
};
smile.config((config) {
config.enableDebugMode = false;
});
smile.userDetails = const UserDetails(
givenNames: 'Ada',
lastName: 'Lovelace',
email: 'ada@example.com',
);
smile.documentVerificationParams =
const DocumentVerificationParams(country: 'ZA');
smile.theme((theme) {
theme.primaryColor = theme.color(
light: const Color(0xFF1A73E8),
dark: const Color(0xFF4DA3FF),
);
theme.buttonShape = 12.0;
});
smile.network((n) {
n.config((c) {
c.jobType = JobType.documentVerification;
c.token = 'your-v3-token';
c.partnerConfig((p) {
p.partnerId = 'your-partner-id';
p.callbackUrl = 'https://example.com/callback';
p.useSandbox = true;
});
c.logging((l) {
l.enabled = true;
l.level = LogLevel.basic;
});
});
n.timeouts((t) {
t.connect = const Duration(seconds: 30);
t.call = const Duration(seconds: 120);
});
});
smile.ml((ml) => ml.analyzers((a) {
a.forCaptureType(CaptureType.selfie);
a.forCaptureType(CaptureType.document);
}));
smile.screens((screens) {
screens.consent((consent) {
consent.partnerName = 'Acme Corp';
consent.partnerIcon = const Icon(Icons.business);
consent.partnerPrivacyPolicyUrl = 'https://acme.com/privacy';
});
screens.instructions();
screens.capture((capture) => capture.captureType = CaptureType.selfie);
screens.capture((capture) {
capture.captureType = CaptureType.document;
capture.document((doc) {
doc.documentType = const GenericDocument();
doc.captureMode = const AutoCapture();
doc.captureBothSides = true;
doc.allowGalleryUpload = false;
});
});
screens.preview((preview) {
preview.allowRetake = true;
});
screens.processing();
});
},
);
}
}