Smile ID Flutter SDK
The Smile ID Flutter SDK runs identity verification inside a Flutter app on Android and iOS. You
compose the journey from screens (consent, instructions, capture, preview, processing) with a
builder, add the on-device analyzer packages for the capture types and platforms you ship, and
receive the submission acknowledgement in onResult. The verification verdict arrives on your
callback URL.
- How it works
- Requirements
- Installation
- Quick start
- Job types
- Builder reference
- Results and errors
- Analytics
- Recipes
- App size
- Troubleshooting
How it works
sequenceDiagram
participant Backend as Your backend
participant App as Your app
participant SDK as UseSmileIDBuilder
participant API as Smile ID
App->>Backend: Ask for a session token
Backend->>API: POST /v3/token (API key, server side only)
API-->>Backend: Short-lived v3 token
Backend-->>App: Token
App->>SDK: Screens, analyzers, token
SDK->>SDK: Consent, capture with on-device analyzers, preview
SDK->>API: Submit the job (processing screen)
API-->>SDK: Submission acknowledgement
SDK-->>App: onResult(UseSmileIDSuccess(JobSubmissionResponse))
API-->>Backend: Verdict on your callback URL
Four rules hold for every integration:
UseSmileIDBuilderis the only entry point. There is no initialisation call and no config file. The builder is a widget; configuration lives in itsbuildercallback.- Authentication is a short-lived v3 token minted by your backend with
POST /v3/token. Your long-lived API key never ships in the app. - There are no predefined products. You compose the screens a job type needs, and the builder validates the composition against that job type before anything renders.
- Results arrive on two paths.
onResultreports that Smile ID accepted the submission. The verdict, which is the source of truth, is delivered asynchronously to your callback URL.
Capture and on-device detection run natively, through the SDK's plugins: ML Kit or Huawei ML on Android and Apple Vision on iOS. Frames never cross into Dart.
Requirements
| Minimum | |
|---|---|
| Flutter | 3.44 |
| Dart | 3.12 |
| Android | API 24 (minSdk), compileSdk 36 (Flutter's default), Android Gradle Plugin 9.1 with a Gradle 9 wrapper |
| iOS | 15.0 |
| Backend | An endpoint that mints v3 tokens with POST /v3/token |
On iOS, add NSCameraUsageDescription to ios/Runner/Info.plist. Contributors: see
CONTRIBUTING.md for the secrets setup the sample app needs.
Installation
The SDK ships as pub packages: the usesmileid entry point plus one analyzer package per capture
type and platform.
dependencies:
usesmileid: ^12.2.0
# Android, on Google Play (GMS) devices
usesmileid_mlkit_face: ^12.2.0
usesmileid_mlkit_document: ^12.2.0
# iOS
usesmileid_vision_face: ^12.2.0
usesmileid_vision_document: ^12.2.0
| Package | Platform | What it adds |
|---|---|---|
usesmileid |
both | The builder, screens, camera, capture and networking |
usesmileid_mlkit_face, usesmileid_mlkit_document |
Android | Face and document detection on ML Kit |
usesmileid_huawei_face, usesmileid_huawei_document |
Android without Google Play services | Face and document detection on Huawei ML |
usesmileid_vision_face, usesmileid_vision_document |
iOS | Face and document detection on Apple Vision |
Add only the capture types you use: a selfie-only app needs no document package. Each analyzer
package is a platform-scoped plugin, so declaring the Android and iOS ones together is safe; only
the running platform's register native code. usesmileid_bridge and
usesmileid_platform_interface come in through usesmileid, so you don't declare them.
Android Gradle Plugin 9
The SDK's Android plugins use AGP's built-in Kotlin, so your app needs Android Gradle Plugin 9.1
or later with a Gradle 9 wrapper. Set the version in android/settings.gradle(.kts):
id("com.android.application") version "9.1.1" apply false
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. An app that
still applies the Kotlin Gradle Plugin also follows Flutter's
built-in Kotlin migration guide,
and keeps the plugin declared in android/settings.gradle(.kts) after the 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
Huawei
For Android devices without Google Play services, use the Huawei packages instead of the ML Kit
ones. They need nothing else: the plugins add Huawei's Maven repository to your Gradle build and
settle the HMS model-dependency manifest attribute between them, so your android/ directory is
unchanged.
Quick start
A SmartSelfie enrollment: consent, a selfie with liveness, and the submission. token is the
short-lived v3 token your backend minted for this session.
import 'package:flutter/material.dart';
import 'package:usesmileid/usesmileid.dart';
class EnrollmentScreen extends StatelessWidget {
const EnrollmentScreen({super.key, required this.token});
final String token;
@override
Widget build(BuildContext context) {
return UseSmileIDBuilder(
builder: (smile) {
smile.network((network) {
network.config((config) {
config.jobType = JobType.smartSelfieEnrollment;
config.token = token;
config.partnerConfig((partner) {
partner.partnerId = 'your-partner-id';
partner.callbackUrl = 'https://your-registered-domain/webhook';
partner.useSandbox = true;
});
});
});
// The running platform's face analyzer, from the packages you installed.
smile.ml((ml) => ml.analyzers((analyzers) => analyzers.forCaptureType(CaptureType.selfie)));
smile.userDetails = const UserDetails(
givenNames: 'Ada',
lastName: 'Lovelace',
email: 'ada@example.com',
);
smile.screens((screens) {
screens.consent((consent) {
consent.partnerName = 'Acme Corp';
consent.partnerIcon = const Icon(Icons.business);
consent.partnerPrivacyPolicyUrl = 'https://acme.example/privacy';
});
screens.instructions();
screens.capture((capture) {
capture.captureType = CaptureType.selfie;
capture.selfie((_) {});
});
screens.preview();
screens.processing();
});
smile.onResult = (result) {
switch (result) {
case UseSmileIDSuccess(:final value):
debugPrint('Submitted job ${value.jobId}');
case UseSmileIDFailure(:final error):
debugPrint('Failed: $error');
case UseSmileIDCancelled():
debugPrint('The user left the flow');
}
};
},
);
}
}
Two behaviours to design around:
- The builder reads its callback once.
UseSmileIDBuilderrunsbuilderwhen it first mounts and keeps the configuration for its lifetime. To start a new session with different inputs, such as a fresh token, give it a new key:UseSmileIDBuilder(key: ValueKey(token), ...). - An invalid flow never renders. The builder validates the flow against its job type before
the first screen paints. When validation fails, it renders an empty surface and
onResultreceives oneUseSmileIDFailurecarrying aBuilderValidationExceptionthat lists every issue. Setsmile.config((c) => c.enableDebugMode = true)during development to see the issues on screen instead.
Job types
Each job type needs a specific composition. Consent is either a consent screen or a pre-supplied
consentInformation, and exactly one of the two.
JobType |
Screens | Builder inputs |
|---|---|---|
smartSelfieEnrollment |
consent, selfie capture, processing | userDetails |
smartSelfieAuthentication |
consent, selfie capture, processing | userDetails, userId from a prior enrollment |
biometricKyc |
consent, selfie capture, processing | userDetails, biometricKYCParams |
documentVerification |
consent, selfie capture, document capture, processing | userDetails, documentVerificationParams |
enhancedDocumentVerification |
consent, selfie capture, document capture, processing | userDetails, enhancedDocumentVerificationParams |
residencyDocumentVerification |
consent, selfie capture, document capture of a passport, processing | userDetails, residencyDocumentVerificationParams |
enhancedKyc |
consent, processing | userDetails, enhancedKYCParams |
bvn |
consent, processing | none |
instructions and preview screens are optional wherever a capture appears. A selfie capture needs
a face analyzer package for the running platform. If your backend binds user details or consent
into the v3 token, the token satisfies those requirements and its values win at submission. A token
that binds consent must bind all four consent fields. See
docs/Flow-Validation-Rules.md for every rule.
Builder reference
Top-level properties
| Property | Type | Description |
|---|---|---|
onResult |
void Function(UseSmileIDResult<JobSubmissionResponse>) |
Called when the flow ends: success, failure or cancellation |
onAnalyticsEvent |
void Function(UseSmileIDAnalyticsEvent)? |
Called for each analytics event. null by default |
userDetails |
UserDetails? |
The end user's names and a contact, sent as user_details |
userId |
String? |
The user from a prior enrollment, for smartSelfieAuthentication |
consentInformation |
ConsentInformation? |
Consent collected earlier; replaces the consent screen |
biometricKYCParams |
BiometricKYCParams? |
idType, idNumber, country, useEnrolledImage |
enhancedKYCParams |
EnhancedKYCParams? |
idType, idNumber, country, and optional bankCode and mobileOperator |
documentVerificationParams |
DocumentVerificationParams? |
country, and an optional idType the server can infer |
enhancedDocumentVerificationParams |
EnhancedDocumentVerificationParams? |
country and idType |
residencyDocumentVerificationParams |
ResidencyDocumentVerificationParams? |
country and idType, which must be 'PASSPORT' |
config
smile.config((config) {
config.enableDebugMode = false;
config.allowOfflineMode = false;
config.enableCrashReporting = true;
config.showAttribution = true;
config.globalPrivacyControl = GlobalPrivacyControl.optOut;
config.exitOrientations = const [DeviceOrientation.portraitUp];
});
| Property | Default | Description |
|---|---|---|
enableDebugMode |
false |
Verbose SDK logging, and the validation screen for an invalid flow. Leave it off in production |
allowOfflineMode |
false |
Writes captures to disk so the job can be submitted when connectivity returns |
enableCrashReporting |
true |
Reports the SDK's own crashes to Smile ID. Set false to opt out |
showAttribution |
true |
The "Powered by Smile ID" mark on every screen that carries it. Document capture never shows it |
globalPrivacyControl |
GlobalPrivacyControl.optOut |
The consent posture for every session. optOut pre-selects consent everywhere; optIn requires an explicit opt-in when the device locale is an EU member state |
exitOrientations |
[DeviceOrientation.portraitUp] |
The orientations applied when document capture, which pins orientation, exits. An app that supports more than portrait sets its own, or [] for the system default |
DeviceOrientation is in package:flutter/services.dart.
theme
smile.theme((theme) {
theme.primaryColor = theme.color(light: const Color(0xFF1A73E8), dark: const Color(0xFF4DA3FF));
theme.primaryForeground = theme.color(light: Colors.white, dark: Colors.black);
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;
theme.cardShape = 16;
});
| Property | Type | Default | Description |
|---|---|---|---|
primaryColor |
AdaptiveColor |
SDK default | Main actions and highlights |
primaryForeground |
AdaptiveColor |
SDK default | Text and icons on the primary colour |
secondaryColor |
AdaptiveColor |
SDK default | Secondary surfaces |
accentColor |
AdaptiveColor |
SDK default | Accents |
fontFamily |
String? |
null, the system font |
The font family name |
buttonShape |
double |
32 |
Button corner radius |
cardShape |
double |
16 |
Card corner radius |
color(light:, dark:) builds an AdaptiveColor that follows the platform brightness. See
docs/Theming.md.
Strings and locales
The SDK ships English for every si_* key (the canonical list is
usesmileid/lib/l10n/intl_en.arb). To translate, add a lib/l10n/intl_<lang>.arb file to your
app and declare it as an asset; the SDK loads it at flow start by device locale. You declare only
the keys you change:
# pubspec.yaml
flutter:
assets:
- lib/l10n/intl_fr.arb
{
"@@locale": "fr",
"si_consent_allow": "Autoriser",
"si_consent_deny": "Refuser"
}
Parameterised strings use named {name} placeholders, the same convention as the Android and iOS
SDKs, so translations copy across. See docs/Localization.md.
network
smile.network((network) {
network.config((config) {
config.jobType = JobType.documentVerification;
config.token = 'your-v3-token';
config.onTokenExpired = (previousToken) async => fetchFreshToken(previousToken);
config.partnerConfig((partner) {
partner.partnerId = 'your-partner-id';
partner.callbackUrl = 'https://your-registered-domain/webhook';
partner.useSandbox = true;
partner.partnerParams = const {'reference': 'order-1234'};
});
config.logging((logging) {
logging.enabled = true;
logging.level = LogLevel.basic;
});
});
network.timeouts((timeouts) {
timeouts.connect = const Duration(seconds: 30);
timeouts.call = const Duration(seconds: 120);
});
network.retry((retry) {
retry.strategy = RetryStrategy.exponential;
retry.maxAttempts = 3;
});
network.cache((cache) {
cache.maxSize = 100 * 1024 * 1024;
});
});
| Block | Property | Default | Description |
|---|---|---|---|
config |
jobType |
none | The job to submit. Required |
config |
token |
'' |
The short-lived v3 token, stamped on every authenticated request |
config |
onTokenExpired |
null |
Future<String> Function(String previousToken). Called on a 401; return a fresh token and the SDK retries the request once. Concurrent 401s share one call. If it throws, or the retry is also rejected, the original 401 surfaces |
partnerConfig |
partnerId |
'' |
Your Smile ID partner ID |
partnerConfig |
callbackUrl |
'' |
Where the verdict is delivered. Blank uses the callback URL configured for your account in that environment |
partnerConfig |
useSandbox |
false |
true for sandbox, false for production |
partnerConfig |
partnerParams |
null |
Your own key-value metadata, sent as partner_params with every job and echoed back on its result |
logging |
enabled, level, redactHeaders |
true, LogLevel.basic, [] |
Network logging: none, basic, headers or body. Credential headers (smileid-token, smileid-partner-id, smileid-api-key, smileid-device-nonce, authorization, cookie, set-cookie) are always masked; redactHeaders masks more |
timeouts |
connect, read, write, call |
60, 60, 60, 120 seconds | Per-request timeouts, as Durations |
retry |
strategy, maxAttempts |
RetryStrategy.none, 3 |
exponential (initialDelay, multiplier, maxDelay, jitter), fixed or none |
cache |
enabled, maxSize |
true, 50 MB |
The HTTP cache |
network.interceptors((i) { i.add(i.gzip()); i.add(yourDioInterceptor); }) adds Dio interceptors
of your own.
ml
Detection runs natively, in the analyzer packages you install. With a package declared,
forCaptureType uses the running platform's default factory:
smile.ml((ml) => ml.analyzers((analyzers) {
analyzers.forCaptureType(CaptureType.selfie);
analyzers.forCaptureType(CaptureType.document);
}));
To choose the face factory yourself, pass it per capture type. Each face package exports one:
MlKitFaceAnalyzerFactory, HuaweiFaceAnalyzerFactory and VisionFaceAnalyzerFactory.
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) {
TargetPlatform.android => const MlKitFaceAnalyzerFactory(),
TargetPlatform.iOS => const VisionFaceAnalyzerFactory(),
_ => throw UnsupportedError('Smile ID supports Android and iOS.'),
};
void registerAnalyzers(UseSmileIDFlowBuilder smile) {
smile.ml((ml) => ml.analyzers((analyzers) {
analyzers.forCaptureType(CaptureType.selfie, (selfie) => selfie.add(selfieAnalyzerFactory()));
}));
}
The document packages have no Dart API: declaring the dependency registers the native document analyzer.
screens
Screens run in the order you declare them.
smile.screens((screens) {
screens.consent((consent) {
consent.partnerName = 'Acme Corp';
consent.partnerIcon = const Icon(Icons.business);
consent.partnerPrivacyPolicyUrl = 'https://acme.example/privacy';
consent.onConsentGranted = (info) => debugPrint('Consent at ${info.grantedAt}');
});
screens.instructions();
screens.capture((capture) {
capture.captureType = CaptureType.selfie;
capture.selfie((selfie) => selfie.enableEnhancedLiveness = true);
});
screens.capture((capture) {
capture.captureType = CaptureType.document;
capture.document((document) {
document.documentType = const GenericDocument();
document.captureMode = const AutoCaptureWithManualFallback(activateManualAfter: Duration(seconds: 10));
document.captureBothSides = true;
});
});
screens.preview((preview) => preview.allowRetake = true);
screens.processing((processing) => processing.showProgressPercentage = true);
});
| Screen | Properties |
|---|---|
consent |
partnerName, partnerIcon (a Widget) and partnerPrivacyPolicyUrl are required. onConsentGranted, allowButton, denyButton |
instructions |
showHeroOval, continueButton |
capture |
captureType (required), and a selfie or document block to match it |
selfie |
enableEnhancedLiveness (active liveness), allowAgentMode (the rear camera, for an assisting agent). The two cannot both be true |
document |
documentType (required), captureMode, captureBothSides (default true, false for a passport), allowSkipBack, allowGalleryUpload, knownIdAspectRatio |
preview |
allowRetake (default true) |
processing |
showProgressPercentage |
The order is validated: consent first when present, instructions before the capture, every preview after a capture, and processing last. Non-capture screens appear at most once.
documentType is Passport(), SouthAfricaGreenBook() or GenericDocument(...). It drives the
frame's aspect ratio and whether a back side exists. captureMode is AutoCapture(),
ManualCapture(), or AutoCaptureWithManualFallback(activateManualAfter:), the default, which
offers the shutter after 10 seconds.
Button slots
A button slot, such as allowButton or continueButton, takes a function from a
ButtonSlotScope to any Widget; call scope.onClick from your button and respect
scope.enabled:
consent.allowButton = (scope) => FilledButton(
onPressed: scope.enabled ? scope.onClick : null,
child: const Text('Accept'),
);
Results and errors
smile.onResult = (result) {
switch (result) {
case UseSmileIDSuccess(:final value):
jobs.track(value.jobId);
case UseSmileIDFailure(:final error):
log(error);
case UseSmileIDCancelled():
onCancel(); // your own handler; the flow has already exited
}
};
UseSmileIDResult<JobSubmissionResponse> is sealed, so a switch handles all three branches.
UseSmileIDSuccess means Smile ID accepted the submission; it is not the verdict. Its value
carries:
| Field | Type | Description |
|---|---|---|
jobId |
String |
The server-issued job ID |
userId |
String |
The server-issued user ID |
status |
String |
The submission status, such as "submitted" |
message |
String |
A human-readable status message |
createdAt |
String? |
When the server accepted the job, in ISO 8601 |
UseSmileIDCancelled means the user left before the flow finished: no job was submitted and there
is no error. The result does not echo your inputs, so keep any captured data or identity fields you
need at the call site.
Errors the SDK raises extend UseSmileIDException, which carries:
errorCode: aUseSmileIDErrorCodewhosecodeis a stable string for grouping, such as"NETWORK_TIMEOUT"or"BUILDER_VALIDATION_ERROR".message: a message safe to show, with no personal data.suggestedFix: what to change, for triage.cause: the underlying error, such as aDioException.
To forward failures to a crash reporter, report the cause and tag the code:
smile.onResult = (result) {
if (result case UseSmileIDFailure(:final error)) {
final smileError = error is UseSmileIDException ? error : null;
Sentry.captureException(
smileError?.cause ?? error,
withScope: (scope) {
scope.setTag('smileid.error_code', smileError?.errorCode?.code ?? 'unknown');
scope.setExtra('smileid.suggested_fix', smileError?.suggestedFix ?? '');
},
);
}
};
Validating inputs early
smile.validate() checks the builder's own blocks (the screens, ml and network) without
building; the job-type and screen rules run when the flow builds, and an invalid flow reports them
through onResult. A ValidationStateInvalid lists every problem, each with its message and
suggestedFix.
Analytics
Set onAnalyticsEvent to receive events as the flow runs. Each event has a type and a flat
Map<String, String> of extras, which every analytics backend accepts:
smile.onAnalyticsEvent = (event) {
analytics.logEvent(name: event.type, parameters: event.extras);
};
Every event carries session_id and timestamp (epoch milliseconds), so you can correlate one
run's events.
event.kind is the same event as a UseSmileIDAnalyticsEventKind, a sealed class with one subclass
per event type and its typed fields. A minor release can add a subclass for a new event, so keep a
default branch:
smile.onAnalyticsEvent = (event) {
switch (event.kind) {
case UseSmileIDFlowCompletedFailure(:final errorMessage):
log(errorMessage);
case UseSmileIDDocumentCaptured(:final side):
trackSide(side.wireName);
default:
break;
}
};
type |
When | Extras |
|---|---|---|
flow_started |
The flow starts | job_type, job_type_id |
screen_viewed |
A screen becomes active | screen_name |
consent_captured |
The user grants consent | decision |
selfie_captured |
The selfie and liveness frames are captured | liveness_image_count |
document_captured |
A document image is captured, one event per image | document_side: front, back or visa |
retake_requested |
The user goes back to redo a step | none |
selfie_session_restarted |
The selfie session restarts after an unsatisfied scan | reason, frames_discarded, duration_ms |
selfie_frame_rejected |
A liveness frame is rejected; Android and iOS only, since the Flutter bridge does not carry per-frame rejections | reason |
job_submitted |
The submission starts | job_type, job_type_id, attempt |
flow_completed |
The flow ends with a result; a cancellation sends none | result, job_id on success, error_message on failure |
job_type is this SDK's own label, such as documentVerification, and differs by platform.
job_type_id is the numeric id, such as 6, and is the same on Android, iOS, Flutter and React
Native, so group on it when one dashboard takes events from several platforms. error_message is
free-form developer text, not an identifier; leave it out of a backend that is not cleared for it.
Recipes
Document verification
A selfie and both sides of an ID, checked against the document.
UseSmileIDBuilder(
builder: (smile) {
smile.network((network) => network.config((config) {
config.jobType = JobType.documentVerification;
config.token = 'your-v3-token';
config.partnerConfig((partner) {
partner.partnerId = 'your-partner-id';
partner.useSandbox = true;
});
}));
smile.ml((ml) => ml.analyzers((analyzers) {
analyzers.forCaptureType(CaptureType.selfie);
analyzers.forCaptureType(CaptureType.document);
}));
smile.userDetails = const UserDetails(givenNames: 'Ada', lastName: 'Lovelace', email: 'ada@example.com');
smile.documentVerificationParams = const DocumentVerificationParams(country: 'GH', idType: 'NATIONAL_ID');
smile.screens((screens) {
screens.consent((consent) {
consent.partnerName = 'Acme Corp';
consent.partnerIcon = const Icon(Icons.business);
consent.partnerPrivacyPolicyUrl = 'https://acme.example/privacy';
});
screens.capture((capture) {
capture.captureType = CaptureType.document;
capture.document((document) {
document.documentType = const GenericDocument();
document.captureBothSides = true;
});
});
screens.capture((capture) {
capture.captureType = CaptureType.selfie;
capture.selfie((_) {});
});
screens.preview();
screens.processing();
});
smile.onResult = (result) { /* ... */ };
},
)
Residency: passport and visa
JobType.residencyDocumentVerification captures the passport data page and then a mandatory visa
page in the same document capture step, and submits both in one job.
residencyDocumentVerificationParams.idTypemust be'PASSPORT'.- The document capture sets
documentType = const Passport(). allowSkipBackstays unset, since the visa page cannot be skipped.
UseSmileIDBuilder(
builder: (smile) {
smile.network((network) => network.config((config) {
config.jobType = JobType.residencyDocumentVerification;
config.token = 'your-v3-token';
config.partnerConfig((partner) {
partner.partnerId = 'your-partner-id';
partner.useSandbox = true;
});
}));
smile.ml((ml) => ml.analyzers((analyzers) {
analyzers.forCaptureType(CaptureType.selfie);
analyzers.forCaptureType(CaptureType.document);
}));
smile.userDetails = const UserDetails(givenNames: 'Ada', lastName: 'Lovelace', email: 'ada@example.com');
smile.residencyDocumentVerificationParams =
const ResidencyDocumentVerificationParams(country: 'AE', idType: 'PASSPORT');
smile.screens((screens) {
screens.consent((consent) {
consent.partnerName = 'Acme Corp';
consent.partnerIcon = const Icon(Icons.business);
consent.partnerPrivacyPolicyUrl = 'https://acme.example/privacy';
});
screens.capture((capture) {
capture.captureType = CaptureType.document;
capture.document((document) => document.documentType = const Passport());
});
screens.capture((capture) {
capture.captureType = CaptureType.selfie;
capture.selfie((_) {});
});
screens.processing();
});
smile.onResult = (result) { /* ... */ };
},
)
The visa is sent as the visa part, next to the passport's document part.
Consent collected earlier
When the user consented in an earlier session, supply it and leave the consent screen out. The two are mutually exclusive:
smile.consentInformation = const ConsentInformation(
granted: true,
grantedAt: '2026-09-01T10:00:00Z',
noticeLanguage: 'EN',
noticePrivacyPolicyUrl: 'https://acme.example/privacy',
);
App 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) | — | — |
The full size tables, including what each provider set adds to a new app, are in the repository README.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
onResult receives a BuilderValidationException and nothing renders |
The flow does not match its job type | Read the exception's issues, or set enableDebugMode to see them on screen. See Job types |
pub get fails on the SDK constraint |
Dart is older than 3.12 | Upgrade Flutter to 3.44 or later |
requires Android Gradle plugin 9.1.0 or higher |
The app is on AGP 8 | Move to AGP 9.1 and a Gradle 9 wrapper, as in Android Gradle Plugin 9 |
| The selfie capture reports no viable analyzer | No face package for the running platform | Add usesmileid_mlkit_face (Android) or usesmileid_vision_face (iOS), or the Huawei package on devices without Google Play services |
| The app crashes when the camera opens on iOS | NSCameraUsageDescription is missing from Info.plist |
Add it with a sentence the user sees in the permission prompt |
| A new token never reaches the flow | The builder read its callback once | Key the widget on the token: UseSmileIDBuilder(key: ValueKey(token), ...) |
401 Unauthorized mid-flow |
The token expired | Set onTokenExpired to fetch a fresh token from your backend |
| The app is left in portrait after document capture | The SDK restores exitOrientations |
Set config.exitOrientations to your app's orientations |
| On an iPad, document capture stays in portrait | The app supports iPad multitasking, so iPadOS keeps it in the orientation it has and never rotates it to the landscape the SDK asks for | Set UIRequiresFullScreen to true in ios/Runner/Info.plist, as the sample does. It opts the whole app out of iPad multitasking (Split View, Slide Over, Stage Manager), and it is a stopgap: verified on iPadOS 27.0 with an app built with the iOS 27 SDK, but Apple deprecated the key in iPadOS 26 and will ignore it in a future release (TN3192), after which the screen may stay portrait again. Flutter still logs Failed to change device orientation … Code=101 when the screen rotates; the rotation happens regardless |
Further reading
- CHANGELOG.md: what changed in each release.
- docs/: the SDK's architecture, starting with docs/Flow-Validation-Rules.md and docs/Network-Architecture.md.
- docs.smileidentity.com: the product documentation, including job types and callback payloads.
License
MIT. See LICENSE.
Libraries
- usesmileid
- UseSmileID Flutter SDK — app-facing package.