Skip to content

Skaletek KYC Flutter Plugin

A comprehensive Flutter plugin for Know Your Customer (KYC) verification services, featuring document scanning, face liveness detection, and identity verification powered by AWS Amplify.

✨ Features

  • πŸ†” Document Verification: Passport, National ID, Driver's License, and more
  • πŸ‘€ Face Liveness Detection: Real-time biometric verification using AWS Amplify
  • πŸ“Έ Camera Integration: Live document capture with auto-detection
  • 🎨 Customizable UI: Branded verification experience
  • πŸ”’ Secure: Enterprise-grade security with AWS infrastructure
  • πŸ“± Cross-platform: iOS and Android support

πŸš€ Quick Start

1. Installation

dependencies:
  skaletek_kyc: ^0.0.27
flutter pub get

2. Platform Setup

πŸ“± Android Setup

Requires Kotlin 2.2.0.

Step 1: Update Project Build Configuration

1.1. Kotlin Version (Project Level)

Set Kotlin to 2.2.0 or higher. Check android/settings.gradle(.kts) for a plugins { } block containing version numbers:

  • Present β€” set the Kotlin version there. Projects created with recent Flutter versions already declare 2.3.20 and need no change.
  • Absent β€” add the buildscript block below to android/build.gradle.

Groovy β€” android/build.gradle

buildscript {
    ext.kotlin_version = '2.2.0'

    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:8.9.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

Kotlin DSL β€” android/build.gradle.kts

buildscript {
    extra.apply {
        set("kotlin_version", "2.2.0")
    }

    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath("com.android.tools.build:gradle:8.9.1")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0")
    }
}

1.2. Update android/app/build.gradle (App Level)

Add one line at the bottom, after the android { } and flutter { } blocks. It applies the SDK's build settings β€” minSdk, core library desugaring, and the compileSdk its dependencies require.

Groovy β€” android/app/build.gradle

apply from: project(':skaletek_kyc').file('skaletek_kyc.gradle')

Kotlin DSL β€” android/app/build.gradle.kts

apply(from = project(":skaletek_kyc").file("skaletek_kyc.gradle"))

Step 2: Permissions (Automatic)

INTERNET and CAMERA are declared in the SDK's manifest and merge into your app. NFC comes with the optional skaletek_kyc_nfc package (see NFC). Nothing to add unless you customise manifest merging.

Step 3: Update MainActivity

Ensure your MainActivity extends FlutterFragmentActivity:

// android/app/src/main/kotlin/com/yourpackage/yourapp/MainActivity.kt
package com.yourpackage.yourapp

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()

🍎 iOS Setup

Step 1: Deployment Target

Set the iOS deployment target to 14.0.

In Xcode: select the Runner target β†’ General β†’ Minimum Deployments β†’ iOS 14.0.

Or in code: set all three occurrences in ios/Runner.xcodeproj/project.pbxproj (Debug, Release, Profile):

IPHONEOS_DEPLOYMENT_TARGET = 14.0;

Step 2: Permissions

Add to ios/Runner/Info.plist. iOS terminates the app without it:

<key>NSCameraUsageDescription</key>
<string>This app needs camera access for document scanning and face verification.</string>

Step 3: NFC (optional)

NFC chip reading ships as a separate package, so apps that don't use it carry no NFC code or permissions. To offer it, add:

dependencies:
  skaletek_kyc_nfc: ^0.0.27

There's no code to write. With the package installed, Passport and National ID flows show the NFC option on supported devices; pass enableNfc: false on KYCCustomization to hide it for a session. Without it, those flows use document upload only and you can skip the rest of this step.

iOS (NFC)

Add the usage description to ios/Runner/Info.plist:

<key>NFCReaderUsageDescription</key>
<string>This app uses NFC to read your passport chip for identity verification.</string>

Then enable the capability once in Xcode:

  1. Open ios/Runner.xcworkspace in Xcode.
  2. Select the Runner target β†’ Signing & Capabilities tab.
  3. Click + Capability and add Near Field Communication Tag Reading.

This automatically creates (or updates) ios/Runner/Runner.entitlements:

<key>com.apple.developer.nfc.readersession.formats</key>
<array>
    <string>TAG</string>
</array>

Note: NFC capability requires an Apple Developer account and a real device β€” NFC is not available in the iOS Simulator.

3. Add ISO 7816 application identifiers (required for e-passport reading)

This step is critical β€” without it the NFC session can start but immediately time out without detecting the chip.

Add the following to ios/Runner/Info.plist (not the entitlements file):

<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
    <string>A0000002471001</string>
    <string>A0000002472001</string>
    <string>00000000000000</string>
</array>

These are the standard ICAO 9303 Application Identifiers used by e-passports. Place them alongside NFCReaderUsageDescription in Info.plist.

NFC troubleshooting

  • "Session timeout" / chip not detected: Ensure com.apple.developer.nfc.readersession.iso7816.select-identifiers with the three AIDs is in Info.plist (not the entitlements file). This is the most common cause of NFC sessions opening but immediately failing.
  • "Failed to connect to NFC chip": Confirm Near Field Communication Tag Reading capability is added in Xcode under Signing & Capabilities, and that Runner.entitlements contains com.apple.developer.nfc.readersession.formats = [TAG].
  • Authentication failed after the chip is detected: Check that the document number, date of birth, and expiry date match the MRZ exactly. Wrong MRZ key fields can feel like NFC detection failure because the chip rejects BAC/PACE authentication.
  • Android detection is inconsistent: Remove thick or metal cases, place the document on a flat surface, and keep the phone still for up to 25 seconds while the reader finds the chip antenna.
  • NFC only works on physical devices β€” not supported in the iOS Simulator.

πŸ“– API Reference

KYCUserInfo

final userInfo = KYCUserInfo(
  firstName: "John",
  lastName: "Doe",
  documentType: DocumentType.passport.value,
  issuingCountry: "USA",
);

KYCCustomization

final customization = KYCCustomization(
  docSrc: DocumentSource.camera.value,
  partnerName: "Your Company",
  logoUrl: "https://example.com/logo.png", // optional
  primaryColor: Colors.blue, // optional
  enableNfc: true, // optional; false = hide NFC. Needs skaletek_kyc_nfc
);

Document Types

Type Description
DocumentType.passport International passport
DocumentType.nationalId National ID card
DocumentType.driverLicense Driver's license
DocumentType.residencePermit Residence permit
DocumentType.healthCard Health/medical card

Document Sources

Source Description
DocumentSource.camera Live camera capture with auto-detection
DocumentSource.file File upload from device gallery

🌐 Environment Configuration

You can now specify the environment for the KYC verification process. This controls which backend endpoints are used for the session.

Supported Environments

  • SkaletekEnvironment.dev
  • SkaletekEnvironment.prod
  • SkaletekEnvironment.sandbox

Usage

SkaletekKYC.instance.startVerification(
  context: context,
  token: "your-token-here",
  userInfo: userInfo,
  customization: customization,
  environment: SkaletekEnvironment.prod, // or .dev, .sandbox
  onResult: (result) {
    // Handle result
  },
);
  • If you do not specify the environment parameter, it defaults to SkaletekEnvironment.dev.

Note:

  • The environment parameter is available in the KYCConfig and is passed through the SDK automatically.
  • The correct API endpoints are selected internally based on the environment you choose.

🌍 Languages

Available in English, French, Spanish, German and Portuguese, with an in-flow language dropdown. The face-liveness screen picks its language differently per platform:

  • iOS follows the in-app dropdown.
  • Android follows the device language.

Verification result (onResult)

The callback receives a typed [KYCResult](lib/src/models/kyc_result.dart): success (bool), status (KYCStatus?), message and errorCode.

KYCStatus values: success, failure, awaitReview (manual review pending β€” handle separately from failure), cancelled, inProgress, pending, completed, reject.

onComplete, which receives a Map<String, dynamic>, still works but is deprecated and will be removed in 1.0.0.


Complete Example

import 'package:flutter/material.dart';
import 'package:skaletek_kyc/skaletek_kyc.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Skaletek KYC Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1261C1)),
      ),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  bool _isVerifying = false;
  KYCResult? _result;

  Future<void> _startVerification() async {
    setState(() {
      _isVerifying = true;
      _result = null;
    });

    await SkaletekKYC.instance.startVerification(
      context: context,
      // Create the session token on your own backend. Never ship your API key
      // inside the app.
      token: 'your-token-here',
      userInfo: KYCUserInfo(
        firstName: 'Whyte',
        lastName: 'Peter',
        documentType: DocumentType.passport.value,
        issuingCountry: 'USA',
      ),
      customization: KYCCustomization(
        docSrc: DocumentSource.file.value,
        partnerName: 'Your Company',
      ),
      environment: SkaletekEnvironment.dev,
      onResult: (result) => setState(() {
        _isVerifying = false;
        _result = result;
      }),
    );
  }

  /// `AWAIT_REVIEW` means the session finished but a person still has to approve
  /// it β€” treat it as its own outcome rather than a failure.
  bool get _isUnderReview => _result?.status == KYCStatus.awaitReview;

  Color get _resultColor {
    if (_result?.success ?? false) return Colors.green;
    return _isUnderReview ? Colors.amber.shade800 : Colors.red;
  }

  String get _resultText {
    final result = _result!;
    return [
      if (result.success)
        'Verification complete'
      else if (_isUnderReview)
        'Under review'
      else
        'Verification failed (${result.status?.value ?? 'unknown'})',
      if (result.message?.isNotEmpty ?? false) result.message!,
      if (result.errorCode?.isNotEmpty ?? false)
        'Error code: ${result.errorCode}',
    ].join('\n');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Skaletek KYC')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.verified_user, size: 80, color: Color(0xFF1261C1)),
            const SizedBox(height: 24),
            const Text(
              'Skaletek KYC SDK Demo',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 32),

            if (_isVerifying)
              const CircularProgressIndicator()
            else
              SizedBox(
                width: double.infinity,
                child: FilledButton(
                  onPressed: _startVerification,
                  child: const Text('Start Identity Verification'),
                ),
              ),

            if (_result != null) ...[
              const SizedBox(height: 24),
              Container(
                width: double.infinity,
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  border: Border.all(color: _resultColor),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Text(
                  _resultText,
                  textAlign: TextAlign.center,
                  style: TextStyle(color: _resultColor),
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

πŸ”§ Troubleshooting

Common Issues

Android build errors:

Error Cause Fix
This version of the Compose Compiler requires Kotlin version … Kotlin version differs from the one the liveness plugin pins Set kotlin_version to 2.2.0
The Kotlin Gradle plugin was loaded multiple times… Same cause, reported as a warning Align Kotlin versions across settings.gradle and build.gradle
Plugin request for plugin already on the classpath must not include a version Versions declared in both settings.gradle and a buildscript block Declare them in one place β€” see Step 1.1
E/GeneratedPluginRegistrant: Error registering plugin face_liveness_detector Expected. The SDK configures AWS at runtime instead of at build time None β€” the app is unaffected

iOS build errors:

  • Verify the iOS deployment target is 14.0 or higher

Plugin "SmithyCodeGeneratorPlugin" from package "smithy-swift" must be enabled before it can be used means the Amplify Swift packages resolved above the pinned versions. In Xcode, File β†’ Packages β†’ Reset Package Caches, then rebuild.

Face liveness:

  • Verify camera permissions are granted
  • Check network connectivity for AWS services

NFC: see Step 3 for entitlements and provisioning.