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.brimsage.co.ls
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
import { readFileSync } from 'node:fs';
import { Agent, fetch } from 'undici';

// AttestID requires your client certificate (mutual TLS) on every API-key request.
const mtls = new Agent({
connect: {
cert: readFileSync(process.env.ATTESTID_CLIENT_CERT_FILE!),
key: readFileSync(process.env.ATTESTID_CLIENT_KEY_FILE!),
},
});

app.post('/attestid-session', requireLogin, async (req, res) => {
const response = await fetch(`${process.env.ATTESTID_API_URL}/v1/kyc/session`, {
method: 'POST',
dispatcher: mtls,
headers: {
'X-API-Key': process.env.ATTESTID_API_KEY!,
'X-Access-Key-Id': process.env.ATTESTID_ACCESS_KEY_ID!,
'Content-Type': 'application/json',
},
// Use your authenticated user's id, never one sent by the app.
body: JSON.stringify({ user_id: req.user.id }),
});
res.status(response.status).json(await response.json());
});

Your backend needs a client certificate before this call works. See Client Certificates (mTLS).


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.brimsage.co.ls',
// 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 face-check bridge ships as a package asset, so your app never touches a CDN or a <link> tag.


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.brimsage.co.ls', // required
getSdkToken: (userId) => myBackend.fetchAttestIDSession(userId), // required
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) { /* ... */ },
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

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? face-check session ID, ready for the face-check 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();

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] + prepare the face check)
|
v
liveness (AttestID face check)
|
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