usesmileid 12.0.0-beta01 copy "usesmileid: ^12.0.0-beta01" to clipboard
usesmileid: ^12.0.0-beta01 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.27+
  • Dart 3.0+
  • Android API 24+ (minSdk); compileSdk 37+
  • iOS 15.0+

Installation #

Add the dependency to your pubspec.yaml:

dependencies:
  usesmileid: ^12.0.0

Then run:

flutter pub get

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';
import 'package:usesmileid_bridge/usesmileid_bridge.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');
          }
        };
        smile.screens((screens) {
          screens.instructions();
          screens.capture((c) => c.captureType = CaptureType.selfie);
          screens.preview();
        });
      },
    );
  }
}

Full Builder Reference #

config — Global settings #

smile.config((config) {
  config.enableDebugMode      = false;
  config.allowOfflineMode     = false;
  config.enableCrashReporting = 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

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 8.0 Corner radius for buttons
cardShape double 12.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 #

smile.ml((ml) {
  ml.analyzers((a) {
    a.forCaptureType(CaptureType.selfie, (analyzers) {
      analyzers.addAnalyzer(FaceDetectorAnalyzer.factory());
    });
    a.forCaptureType(CaptureType.document, (analyzers) {
      analyzers.addAnalyzer(DocumentDetectorAnalyzer.factory());
    });
  });
});

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.showAttribution         = true;
    consent.onConsentGranted        = (info) => print('Granted: $info');
  });
  screens.instructions((instructions) {
    instructions.showAttribution = true;
  });
  screens.capture((capture) {
    capture.selfie((selfie) {
      selfie.allowAgentMode         = false;
      selfie.enableEnhancedLiveness = true;
    });
  });
  screens.capture((capture) {
    capture.captureType = CaptureType.document;
    capture.document((doc) {
      doc.autoCapture        = true;
      doc.autoCaptureTimeout = const 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, showAttribution, onConsentGranted, allowButton, denyButton (partnerIcon and partnerPrivacyPolicyUrl are required)
Instructions screens.instructions() showAttribution
Selfie capture screens.capture((c) => c.captureType = CaptureType.selfie) allowAgentMode, enableEnhancedLiveness
Document capture screens.capture((c) => c.captureType = CaptureType.document) autoCapture, allowGalleryUpload, captureBothSides, allowSkipBack, knownIdAspectRatio
Preview screens.preview() allowRetake
Processing screens.processing() showProgressPercentage

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');
      }
    };
    // ... configure flow
  },
)

Payload #

UseSmileIDResult<JobSubmissionResponse> is a sealed class with UseSmileIDSuccess and UseSmileIDFailure branches. 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';
import 'package:usesmileid_bridge/usesmileid_bridge.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');
          }
        };
        smile.onAnalyticsEvent = (event) {
          print('[SmileID] ${event.type} — session: ${event.extras['session_id']} ${event.extras}');
        };

        smile.config((config) {
          config.enableDebugMode = false;
        });

        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.screens((screens) {
          screens.consent((consent) {
            consent.partnerName             = 'Acme Corp';
            consent.partnerPrivacyPolicyUrl = 'https://acme.com/privacy';
          });
          screens.instructions();
          screens.capture((capture) {
            capture.captureType = CaptureType.document;
            capture.document((doc) {
              doc.autoCapture        = true;
              doc.captureBothSides   = true;
              doc.allowGalleryUpload = false;
            });
            },
          );
          screens.preview((preview) {
            preview.allowRetake = true;
          });
          screens.processing();
        });
      },
    );
  }
}
0
likes
0
points
1.86k
downloads

Publisher

verified publishersmileidentity.com

Weekly Downloads

Official Smile ID Flutter SDK for identity verification: selfie capture, liveness checks, and document verification powered by on-device ML.

Homepage
Repository (GitHub)
View/report issues

Topics

#biometric #kyc #identity-verification #face-detection #document-capture

License

unknown (license)

Dependencies

camera, crypto, dio, flutter, flutter_svg, image, image_picker, lottie, meta, screen_brightness, usesmileid_bridge, usesmileid_platform_interface, wakelock_plus

More

Packages that depend on usesmileid