Flutter SDK
The Flutter SDK opens the full verification flow as a modal sheet via MyazaKYC.show(), with on-device native liveness (Apple Vision on iOS, Google ML Kit on Android). It uses a publishable (pk_) key and detects the environment automatically from the key prefix (pk_test_* → sandbox, pk_live_* → production). For shared concepts (supported countries, branding, results, and errors), see Client SDKs.
Feature availability. This SDK is at full parity with the web and React Native SDKs: workflow embedding (
workflowId, ≥ 2.2.0; earlier versions need a placeholdercountryto compile a workflow mount), the capture add-ons (contact OTP, proof of address, questionnaire), business (KYB) verification, any Global Documents country, plus NFC chip reading, which the web SDK can't do at all (browsers can't talk to a passport chip). See Feature availability.
Install
Add the dependency to your pubspec.yaml, then run flutter pub get:
dependencies:
myaza_kyc_sdk_flutter: ^2.2.0Requirements
| Requirement | Minimum |
|---|---|
| Flutter | 3.27 (Dart 3.6) |
| iOS deployment target | 13.0 |
Android minSdkVersion | 21 (Android 5.0) · compileSdk 34 |
Face detection runs on-device (Apple Vision on iOS, Google ML Kit on Android: an Android-only Gradle dependency, so there's no cross-platform ML Kit iOS pod and the SDK still builds on Apple-Silicon iOS simulators). Add the camera permission on both platforms (there is no microphone permission: voice guidance is text-to-speech output only):
<!-- iOS: ios/Runner/Info.plist -->
<key>NSCameraUsageDescription</key>
<string>We use the camera to photograph your ID and capture a live selfie.</string><!-- Android: android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />If your workflows use the Address Intelligence step, also add the location strings. Both are best-effort (they power the "Use my current location" shortcut and the attest fix; the pin always works by dragging alone), but iOS crashes on the permission request if the usage string is missing:
<!-- iOS: ios/Runner/Info.plist -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location helps place the map pin on your address.</string><!-- Android: android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />Presence reporting (Address Intelligence)
When a workflow enables presence verification, the SDK stores the confirmed pin on-device at capture. Call the reporter from your app on a natural moment (app open works well):
final result = await MyazaAddressPresence.report(
apiKey: 'pk_live_…',
externalUserId: 'user_42', // the same userId the KYC flow ran with
);
// result.reason: reported | noPin | noFix | outsideFence | networkErrorIt never throws and never blocks startup. The geofence is evaluated on-device: only the derived day + night flag is transmitted, never a coordinate. A fix outside the fence sends nothing; a mock-location fix is reported flagged. clearPresencePin(externalUserId) drops the stored pin. Stored pins self-expire after 45 days for bounded checks; a pin captured under always-on monitoring never expires until revoked.
Background monitoring (native geofencing)
The stronger tier: the OS wakes the plugin's native side on fence crossings, app closed or not — on Android the fence survives reboots, and iOS relaunches the app for crossings by itself. Entries stamp; exits fold the dwell into per-day aggregates natively and flush them.
Declare the background-location entries in your own manifest and Info.plist first (the plugin never adds them for you, because the declaration changes your store review posture — the Background Location Declarations page carries the ready-to-paste texts). Then:
final result = await MyazaBackgroundPresence.enable(
apiKey: 'pk_live_…',
externalUserId: 'user_42',
);
// result.reason: started | noPin | permissionDenied | backgroundDenied | unavailableenable() walks the two-step permission escalation (while-in-use, then "allow all the time"); MyazaBackgroundPresence.disable() disarms and forgets. A refusal leaves the foreground tier working exactly as before.
Which tier is running?
final status = await presenceStatus('user_42');
// status.tier: PresenceTier.background | foreground | none — plus pinStored,
// alwaysOn, both permission states and geofenceArmed
if (status.tier == PresenceTier.none && status.pinStored) {
await openLocationSettings(); // the only road back after a denial
}Usage
MyazaKYC.show() opens the full flow as a modal bottom sheet (a full-screen page on Android). Note that context is a named parameter, and the callbacks are passed to show() alongside config, not inside it.
Recommended: mount a workflow
Build the flow once in the dashboard as a workflow, then mount it by id. The country, ID types, capture steps, branding and copy all come from the workflow, so changing the flow is a re-publish rather than an app release, which matters more on mobile, where a redeploy means an app-store round trip.
import 'package:flutter/material.dart';
import 'package:myaza_kyc_sdk_flutter/myaza_kyc_sdk_flutter.dart';
void startKYC(BuildContext context) {
MyazaKYC.show(
context: context,
config: const MyazaKYCConfig(
apiKey: 'pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
workflowId: 'wf_AbC123dEf456',
// Runtime data — a workflow is a shared template and cannot carry any of it.
userId: 'user_42',
userData: UserData(firstName: 'Jane', lastName: 'Doe'),
metadata: {'requestId': 'order_1001'},
),
onSubmit: (submission) => debugPrint('Submitted: ${submission.verificationId}'),
onError: (error) => debugPrint('Error: ${error.code} — ${error.message}'),
onClose: () => debugPrint('KYC closed'),
);
}userData is worth passing. It is the name you believe the user has, and it is compared against the name read off their document, and that comparison is what produces dataMatch on the verification. Leave it out and the check simply never runs: there is nothing to compare the document against, and dataMatch comes back null.
It cannot live on the workflow. userId, userData and metadata are per-user runtime values, and a workflow is a template shared by every visitor, so these stay in code even when everything else moves to the dashboard.
countryis optional here: the resolved workflow carries it, exactly as in the React SDK. Pass one only when you are not using a workflow.
Or configure everything in code
Skip the workflow and pass the flow's shape in MyazaKYCConfig. Useful for a quick start or a single fixed flow; anything you'd change later means an app release.
import 'package:flutter/material.dart';
import 'package:myaza_kyc_sdk_flutter/myaza_kyc_sdk_flutter.dart';
void startKYC(BuildContext context) {
MyazaKYC.show(
context: context,
config: MyazaKYCConfig(
apiKey: 'pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
country: 'NG',
idTypes: const ['bvn', 'drivers-license', 'passport'],
userData: const UserData(firstName: 'Jane', lastName: 'Doe'),
enableSelfie: true,
enableDocumentCapture: true,
enableLiveness: true,
appearance: const MyazaKYCAppearance(
primaryColor: Color(0xFF5645F5),
companyName: 'Myaza',
logo: 'default',
theme: MyazaThemeMode.light,
),
consent: const KYCConsentContent(
title: 'Welcome, {firstName}',
description: "A quick check to confirm it's really you.",
),
success: const KYCSuccessContent(
title: "You're all set, {firstName}!",
description: "We'll email you once your verification is reviewed.",
),
metadata: const {'requestId': 'order_1001', 'userId': 'user_42'},
),
onSubmit: (submission) {
// The verification was created; submission.status is always 'pending'.
// The final result arrives via webhook to your backend (or fetch it with a
// secret-key GET /verifications/:id call, never from the client).
debugPrint('Submitted: ${submission.verificationId}');
},
onError: (error) {
// Technical errors only (network / 401 / 402 / upload).
debugPrint('Error: ${error.code} — ${error.message}');
},
onClose: () => debugPrint('KYC closed'),
);
}Config (MyazaKYCConfig)
| Field | Type | Default | Description |
|---|---|---|---|
apiKey | String | — | Required. Sent as Authorization: Bearer. The environment is derived from the key prefix (pk_test_* → sandbox, pk_live_* → production); an unrecognised prefix throws. |
workflowId | String? | — | Run a published workflow (wf_…) built in the dashboard. The SDK resolves its configuration on launch and uses it as the source of truth: workflow config wins over overlapping fields. Makes country optional. Requires ≥ 2.2.0. |
country | String? | — | Required unless workflowId is set. ISO-2 country whose ID types are offered ('NG', 'GH', …). Any ISO country works: the org's grants are enforced server-side. |
countries | List<WorkflowCountryOption>? | — | Multi-region. More than one entry inserts a country-select step; the picked entry's idTypes win. Usually set by a workflow. |
idTypes | List<String>? | all for country | Subset of ID type keys to offer (['bvn', 'passport'], the same kebab-case keys as the React SDKs); null shows everything enabled for the country. |
userId | String? | — | Your stable reference for the person being verified: repeat checks of the same userId collapse onto one entity, and it's how you correlate results back to your record. |
userData | UserData? | — | Pre-fills the user's details. |
enableSelfie | bool | true | Capture a selfie during liveness. |
enableDocumentCapture | bool | true | Enable the document-scan step for document IDs. |
allowDocumentUpload | bool | true | Allow picking a document photo from the device gallery instead of the camera. false hides the "upload instead" option, except on the camera-permission-denied screen, where it stays as an escape hatch. |
enableLiveness | bool | true | Run the liveness challenge step. The server can still disable it per ID type. |
livenessMode | String | 'gestures' | How liveness proves presence: 'gestures', 'flash' (screen-reflection), or 'both'. Usually set by a workflow. |
deviceIntelligence | bool | true | Collect device + IP fraud signals (Device Intelligence). |
voiceGuidance | VoiceGuidanceConfig | enabled (en-US) | Spoken liveness instructions (accessibility, TTS output, no microphone). VoiceGuidanceConfig.off mutes it; VoiceGuidanceConfig(language: 'fr-FR') sets the voice language. |
showThemeToggle | bool | true | Show a light/dark toggle in the header. Set false to hide it. The flow then stays on appearance.theme and the user can't switch it. |
disableClose | bool | false | Hide the close (X) button and block all user dismissal (swipe-down drag, Android back, barrier tap). The flow can then only be closed programmatically by popping the route MyazaKYC.show() returns (its Future completes on close). The terminal "Submitted" step is non-dismissible regardless. |
appearance | MyazaKYCAppearance? | brand defaults | Brand & theme the flow: colours, logo, light/dark. See Branding & theming. |
consent | KYCConsentContent? | built-in copy | Override the consent/welcome screen title and description. See Consent screen copy. |
success | KYCSuccessContent? | built-in copy | Override the success/submitted screen title and description. See Success screen copy. |
metadata | Map<String, dynamic>? | — | Forwarded with the verify request (include your requestId). |
livenessConfig | LivenessConfig? | 2 challenges, 8s each | Tune the liveness challenge sequence (see below). |
UserData accepts firstName, lastName, dateOfBirth, gender, address, and phoneNumber (all optional).
Callbacks
Passed to MyazaKYC.show() alongside config:
| Callback | Type | Description |
|---|---|---|
onSubmit | void Function(KYCSubmission) | Called when the server accepts the verification. status is always 'pending'. |
onError | void Function(KYCError) | Called for technical errors only: receives a typed KYCError (code, message, optional details). Verification outcomes don't come through here. See Errors. |
onClose | void Function() | Called when the user closes the flow. |
Countries & ID types are plain strings
country takes any ISO-2 code ('NG', 'GH', 'FR', …) and idTypes takes the same kebab-case keys as the React SDKs ('bvn', 'drivers-license', 'ghana-card', …). See the ID types catalogue. There is no Country/IdType enum to import. The one enum you'll meet is MyazaThemeMode (light / dark) on appearance.theme.
Liveness configuration
LivenessConfig tunes the active-liveness step. Defaults match the web SDK.
| Field | Type | Default | Description |
|---|---|---|---|
challengeCount | int | 2 | Number of gesture challenges drawn from the pool. |
challengePool | List<ChallengeConfig>? | kDefaultChallengePool | The set of challenges to draw from. |
timeoutPerChallenge | int | 8 | Seconds allowed per challenge before it fails. |
enableAvatar | bool | true | Show the animated avatar that demonstrates each gesture. |
The default pool covers four LivenessChallenge gestures: nod, turn, blink, and smile.