usesmileid 12.1.1 copy "usesmileid: ^12.1.1" to clipboard
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); compileSdk 36+ (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.body only 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.


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();
        });
      },
    );
  }
}