Skip to main content

Using the Flutter SDK

The attestid_sdk package provides Flutter components and APIs for performing identity verification, including document capture, image quality validation, optional passport NFC chip scanning, face liveness detection, and verification result retrieval.

Supported platforms:

  • Android (including passport NFC chip scanning)
  • iOS
  • Flutter Web

Prerequisite: Before integrating the SDK, ensure you have obtained your access_key_id and api_key. See Creating a Company & Getting Your Keys.


Requirements

ToolMinimum version
Flutter3.32.0
Dart3.8.0
AndroidAPI 24 (Android 7.0)
Xcode / iOS14+

1. Install

# pubspec.yaml
dependencies:
attestid_sdk: ^0.1.0
flutter pub get

2. Environment setup

Your api_key is a server-side credential and must not be included in your Flutter application. A .env file loaded via flutter_dotenv still ships inside the compiled APK/IPA as a plain asset — anyone can unzip it out or pull the string with a decompiler. There is no build variant of a Flutter app where apiKey is safe to hold, so AttestIDConfig doesn't accept it at all — only a getSdkToken callback that fetches an already-minted sdk_token from your own backend, which holds the key server-side.

# .env (never commit this file, and notice: no API key here at all)
ATTESTID_API_URL=https://api.attestid.io
MY_BACKEND_URL=https://api.yourcompany.com

The following example uses Node.js and Express. The same flow can be implemented using any server-side framework.

// your-backend/routes/attestid-session.ts
app.post('/attestid-session', async (req, res) => {
const response = await fetch(`${process.env.ATTESTID_API_URL}/v1/kyc/session`, {
method: 'POST',
headers: {
'API-Key': process.env.ATTESTID_API_KEY!,
'Access-Key-Id': process.env.ATTESTID_ACCESS_KEY_ID!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: req.body.userId }),
});
res.status(response.status).json(await response.json());
});

3. Quick start — zero setup

For new applications, AttestIDApp.run() provides a complete verification experience with minimal configuration.

// lib/main.dart
import 'dart:convert';
import 'package:attestid_sdk/attestid.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;

Future<void> main() async {
await dotenv.load(fileName: '.env');
await AttestIDApp.run(
apiUrl: dotenv.env['ATTESTID_API_URL'] ?? '',
// Calls your own backend, which holds the AttestID API key server-side
// and calls POST /kyc/session on your behalf.
getSdkToken: (userId) async {
final res = await http.post(
Uri.parse('${dotenv.env['MY_BACKEND_URL']}/attestid-session'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'userId': userId}),
);
return SdkSessionResponse.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
},
sandbox: true, // shows a "sandbox" badge — set false for production
);
}

AttestIDApp provides the full home screen, AppBar, verification flow, and result display out of the box.


4. Embedding into an existing app

If you already have a MaterialApp, use AttestIDHomeScreen or push AttestIDPage directly onto your navigator.

Home screen integration

MaterialApp(
home: AttestIDHomeScreen(
client: AttestID(AttestIDConfig(
apiUrl: 'https://api.attestid.io',
// Never embed apiKey here — call your own backend instead (see step 2 above).
getSdkToken: (userId) => myBackend.fetchAttestIDSession(userId),
)),
sandbox: false,
),
)

Option: Verification page

Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (_) => AttestIDPage(
client: _client,
onResult: (KYCResult result) {
if (result.verified) navigateToSuccess();
},
),
),
);

Option: Verification widget

AttestIDVerifier(
controller: AttestIDController(client),
defaultUserId: currentUser.id,
onResult: (KYCResult result) { /* ... */ },
onError: (String msg) { /* ... */ },
enableNfcChipScan: true, // default — set false to skip the NFC step entirely
)

Note that sandbox means different things depending on the widget: on AttestIDApp / AttestIDHomeScreen it just shows a "sandbox" badge in the app bar. On AttestIDPage / AttestIDVerifier it controls whether the User ID field is shown and editable — leave it false in production so the field stays hidden and defaultUserId is used silently.

Passport NFC chip scan

When enableNfcChipScan is true (the default) and the device is Android, a passport verification walks through two extra steps — shown in the step bar as MRZ and NFC Chip — between the document photo and the face-liveness check:

  1. Document — capture/upload the passport photo, same as any other document type.
  2. MRZ — review the document number, date of birth, and expiry date. These are auto-filled from on-device OCR of the photo's MRZ line; correct anything misread, or fill them in yourself if OCR failed. They're only used to unlock the chip (BAC key) — nothing here is sent to your backend directly.
  3. NFC Chip — scan the passport's chip (reading DG1/MRZ and DG2/face photo via Basic Access Control). A successful scan swaps in the chip's face photo (more trustworthy than the camera capture) and shows a "Verified via passport chip" badge.
  4. Only then does "Continue to Face Check" call startVerification(), sending the chip's MRZ fields alongside the photo via StartKYCOptions.chipData — omitted entirely when no chip scan happened.

The MRZ/NFC steps only appear for IdType.passport on Android (with enableNfcChipScan true) — everything else (national ID, driver's licence, or any document on iOS/web) goes straight from Document to Face Check, unchanged.

On a device that can actually scan (NFC hardware present and turned on), the NFC Chip step's continue button stays disabled until the chip read succeeds. On an Android device without usable NFC (no hardware, or turned off), the MRZ/NFC steps still appear, but explain why scanning isn't available and let the user continue with just the photo instead.

Set enableNfcChipScan: false to skip both extra steps everywhere (e.g. an iOS-only rollout, or while your backend doesn't yet consume chip data) — submission then only ever needs the photo, same as national ID / driver's licence.

You don't need to add the android.permission.NFC manifest permission yourself — it's merged in automatically by the SDK's NFC dependency.


5. Platform setup

Android

android/app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-feature android:name="android.hardware.camera.front" android:required="true"/>

<!-- Image picker: Android <= 12 -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32"/>
<!-- Image picker: Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>

android/app/build.gradle.kts — the SDK's face liveness detector requires minSdk = 24 and Java 8+ desugaring:

android {
defaultConfig {
minSdk = 24
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}

dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

Your MainActivity can stay a plain FlutterActivity() — the SDK's own plugin class handles the native liveness method channel and manifest merging (camera, NFC) automatically, since attestid_sdk is a real Flutter plugin.

iOS

ios/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>Camera is required for document capture and liveness detection.</string>

Passport NFC chip scanning is Android-only; iOS verifications always go straight from Document to Face Check.

Web

Add to web/index.html (before </head>):

<script src="assets/packages/attestid_sdk/assets/web/liveness_bridge.js"></script>

That's the only markup needed — the bridge (a self-contained Amplify FaceLivenessDetector build) ships as a package asset, so your app never touches a CDN, a <link> tag, or Amplify directly.


6. API reference

Widgets — integration levels

WidgetUse case
AttestIDAppComplete MaterialApp — call AttestIDApp.run() from main()
AttestIDHomeScreenFull home screen — embed in your existing MaterialApp
AttestIDPageFull-screen verification page — push onto your navigator
AttestIDVerifierVerification widget — embed inside your own Scaffold
AttestIDResultCardPass/fail result banner — display KYCResult anywhere
AttestIDLaunchButtonStyled CTA button — auto-switches "Start" / "Verify Again" / "Done"

AttestIDApp

AttestIDApp(
apiUrl: 'https://api.attestid.io', // required
getSdkToken: (userId) => myBackend.fetchAttestIDSession(userId), // required
identityPoolId: 'us-east-1:...', // optional
sandbox: false, // shows "sandbox" badge
appTitle: 'My App', // MaterialApp title + AppBar
subtitle: 'KYC Verification', // AppBar sub-label
description: 'Verify your identity...', // body copy
enableNfcChipScan: true, // offer passport NFC chip scan (Android only)
)

// Static entry point
await AttestIDApp.run(apiUrl: '...', getSdkToken: (userId) => myBackend.fetchAttestIDSession(userId));

AttestIDHomeScreen

AttestIDHomeScreen(
client: myClient, // required AttestID instance
sandbox: false,
title: 'My App',
subtitle: 'KYC Verification',
description: 'Verify your identity...',
enableNfcChipScan: true,
)

AttestIDPage

AttestIDPage(
client: myClient, // required
onResult: (r) { /* ... */ }, // required
defaultUserId: 'user_123',
defaultIdType: IdType.passport,
title: 'Identity Verification',
onError: (msg) { /* ... */ },
sandbox: false, // shows/hides the User ID field
enableNfcChipScan: true,
)

AttestIDVerifier

AttestIDVerifier(
controller: myController, // required AttestIDController
onResult: (r) { /* ... */ }, // required
defaultUserId: 'user_123',
defaultIdType: IdType.nationalId,
onError: (msg) { /* ... */ },
livenessProvider: const AwsLivenessProvider(), // pluggable — see LivenessProvider below
deviceId: null, // optional, for fraud-risk device fingerprinting
sandbox: false, // shows/hides the User ID field
enableNfcChipScan: true,
)

AttestIDResultCard

AttestIDResultCard(result: kycResult)

Shows a green card for verified == true, red for false. Displays the full name from result.document or falls back to verificationId.

AttestIDLaunchButton

AttestIDLaunchButton(
onTap: _startVerification,
hasResult: _result != null, // switches label to "Verify Again"
isVerified: _result?.verified == true, // switches label to "Done" and disables the button
)

AttestIDConfig

FieldTypeDescription
apiUrlStringBase URL of your AttestID backend
getSdkTokenFuture<SdkSessionResponse> Function(String userId)Fetches a short-lived sdk_token from your own backend — never construct this from an in-app apiKey; see step 2 above
identityPoolIdString?Cognito Identity Pool ID; optional — getLivenessCredentials() can fetch it from the backend instead
regionStringAWS region, defaults to us-east-1

AttestIDController

AttestIDController (a ChangeNotifier) is the state machine AttestIDVerifier drives internally — use it directly for a fully custom UI.

final controller = AttestIDController(client);

controller.state // VerifyState enum
controller.isLoading // bool
controller.error // String?
controller.qualityIssues // List<String> — populated when the photo-quality gate fails
controller.result // KYCResult? — set when state == done
controller.livenessError // String? — a technical liveness-SDK error, distinct from a failed check
controller.rawLivenessSessionId // String? — provider-prefix stripped, ready for the liveness widget

await controller.startVerification(options);
await controller.completeVerification(deviceId: null);
await controller.retryLiveness(); // re-runs the liveness step using the last startVerification() options
controller.notifyLivenessError('message'); // records a liveness error without leaving the liveness step
controller.reset();

LivenessProvider

Liveness rendering is pluggable via the abstract LivenessProvider class — AttestIDVerifier defaults to const AwsLivenessProvider() (AWS Rekognition Face Liveness, via the native Amplify SDK on Android/iOS and the JS bridge on web). Implement LivenessProvider.buildWidget() yourself to swap in another provider.

KYCResult

FieldTypeDescription
verifiedboolOverall pass/fail
verificationIdStringUnique verification ID
statusStringRaw status from the API
documentDocumentInfo?Extracted document fields
checksChecksInfo?Liveness + face match results
failureReasonString?Human-readable reason when failed

IdType enum

IdType.passport
IdType.nationalId
IdType.driversLicence

PassportChipData

Returned by a successful NFC chip scan and passed as StartKYCOptions.chipData. Data read directly off the chip (DG1 MRZ + optional DG2 face photo) via Basic Access Control — more trustworthy than OCR since it's signed by the issuing authority.

FieldTypeDescription
documentNumber, firstName, lastName, nationality, country, genderStringMRZ fields read from the chip
dateOfBirth, dateOfExpiryDateTimeFrom the chip
faceImageBytesList<int>?Raw chip face photo (DG2), if present
faceImageIsJpegbool?true if faceImageBytes is JPEG (displayable); false means JPEG2000, which the SDK cannot decode

7. Verification flow

When using AttestIDVerifier, user consent is collected before verification begins. If you are building a custom integration, you are responsible for collecting consent and providing StartKYCOptions.consentGivenAt.

idle
| startVerification()
v
qualityCheck (client-side blur / brightness / size / face-count checks)
|
v
[ passport on Android with enableNfcChipScan: true only ]
| mrz (review/correct OCR'd document number, DOB, expiry)
v
| nfc (scan the chip — required if the device supports NFC and it's on)
v
uploading (parallel: upload ID photo [+ chip data] + fetch liveness credentials)
|
v
liveness (face liveness detection — provider from LivenessProvider)
|
v
completing (fetch final result from the API)
|
v
done (KYCResult available)

A failed liveness check (as opposed to a failed verification, e.g. a technical camera/SDK error) surfaces via controller.livenessError and can be retried in place with controller.retryLiveness(), without redoing the document step.


8. Photo quality thresholds

CheckThreshold
Minimum file size30 KB
Blur (Laplacian variance)below threshold flags "too blurry"
Brightnessflags too dark / overexposed outside the accepted range
Passport two-page spreadwide aspect ratio warns "wrong page photographed"
Document type mismatchaspect-ratio check flags an obvious passport-vs-card mismatch against the selected IdType
Face countML Kit on-device detection (Android/iOS only) flags zero or multiple faces on the document photo

9. Error handling

import 'package:attestid_sdk/attestid.dart';

try {
await client.startVerification(options);
} on AuthError catch (e) {
// Invalid/expired sdk_token (HTTP 401/403)
} on KYCError catch (e) {
// Any other non-auth API error
} on CameraError catch (e) {
// Camera access or frame capture failed
if (e.permanentlyDenied) {
// permission denied and the OS won't re-prompt — send the user to Settings
}
} on ValidationError catch (e) {
// Rejected client-side before the API was ever called, e.g. not a genuine JPEG/PNG
} on AttestIDError catch (e) {
// Base class — e.code / e.message / e.statusCode
}

AttestIDController surfaces the same errors via controller.error (a message string) rather than exceptions, so AttestIDVerifier-based UIs don't need a try/catch.


Example application

See Flutter Demo App for a complete working example, including an example backend proxy and platform run instructions.


Support

For API keys, onboarding, native platform setup files, or technical support: support@brimsage.com