
# Getting started: Flutter (Validations plugin)
{{<sdk-guides-aside>}}
{{<tags>}}

# **Introduction**

The Truora Flutter plugin allows you to integrate identity verification features directly into your Flutter mobile applications. The plugin bridges to native implementations on both Android and iOS platforms, handling the complexity of capturing the user's identity documents, facial recognition, and liveness detection to verify their identity against backend records.

# **Requirements**

## **Platform Requirements**

* **Dart: From 3.1.0 to 4.0.0**   
* **Flutter: 3.0.0 or higher**    
* **Android:** 5.1 (API Level 21\) or higher  
* **iOS:** 13.0 or higher

## **Permissions Required**

* Camera access (for document and face capture)  
* Internet access (for API communication)

# **Prerequisites & Authentication**

## **Android Setup: FlutterFragmentActivity (Required)**

The Android side of the plugin bridges to the native Truora SDK, which requires the host activity to be a `FragmentActivity`. The default `FlutterActivity` generated by `flutter create` extends `Activity` directly and is **not compatible** — the plugin will refuse to initialize and `start()` will fail with the error `ValidationHandler not available. The host MainActivity must extend FlutterFragmentActivity ...`.

To fix this, switch your `MainActivity` to `FlutterFragmentActivity`. This is a one-line change in `android/app/src/main/kotlin/.../MainActivity.kt`:

{{<code lang="kotlin" lang_title="Kotlin">}}
package com.your.app

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()
{{</code>}}

`FlutterFragmentActivity` is provided by Flutter and is a drop-in replacement for `FlutterActivity` for any plugin that needs an AndroidX `FragmentActivity` host. No other changes are required.

If you have a custom `MainActivity` (e.g. extending `AppCompatActivity` for legacy reasons), that also works — anything that extends `androidx.fragment.app.FragmentActivity` is accepted.

## **The Key Provider Interface**

To prevent hardcoding sensitive logic, you must implement the TruoraAPIKeyGetter interface in your code. This allows you to retrieve the temporary generated API key from a secure location (like a compiled secret, a secure storage enclave, or an obfuscated string). This interface is how the sdk will get access to the api key for the validations, the resulting api key must be of type ‘sdk’ (temporary), any other key type will result in an error.

### Sample Dart Implementation

{{<code lang="dart" lang_title="Dart">}}
/// A secure API key provider for the Truora plugin.
/// In production, retrieve this from secure storage or a backend endpoint.
class SecureApiKeyProvider {
  final String apiKey;

  SecureApiKeyProvider(this.apiKey);

  Future<String> getApiKeyFromSecureStorage() async {
    // Implementation detail: In a production environment, you should
    // retrieve this from Flutter Secure Storage or a backend API.
    // For example:
    // final storage = FlutterSecureStorage();
    // return await storage.read(key: 'truora_api_key') ?? '';
    return apiKey;
  }
}
{{</code>}}

# **Configuration Options**

Using custom validation config and UI config is optional when building the validation.

If no UI config is provided, then the validation view will show the standard Truora branding, logo and color, and if no validation config is provided then default values will be applied.

More indepth information on the validation configuration options, such as similarity threshold, can be found in the official validations api documentation

- [Face validation API](https://dev.truora.com/validators/face_validation_api/)  
- [Document validation API](https://dev.truora.com/validators/document_validation_api/)

## **User ID**

The `userId` parameter is crucial for initializing the Truora SDK. It provides a unique identifier for the user undergoing the validation process.

* Purpose: The `userId` allows Truora to associate validation attempts and reference data (like previously captured reference faces) with a specific user in your system. It is mandatory for linking the validation results to your user base.

* Best Practice: This ID should be an immutable, non-sensitive identifier from your database (e.g., a UUID or internal user ID) that uniquely identifies the person. Do not use sensitive PII like an email address or national ID number as the `userId`.

## **Language**

The `language` parameter allows you to define the localization of the text and instructions displayed within the validation view.

* Customization: By setting the language, you ensure a user-friendly experience by presenting instructions and feedback in the user's preferred language.

* Available Options: The SDK supports several languages. You should provide the language code as a two-letter string (e.g., "es" for Spanish, "en" for English, "pt" for Portuguese). If no language is provided, the SDK will typically default to English or attempt to use the device's system language, though providing an explicit language is recommended.

### Example in Initialization:

{{<code lang="dart" lang_title="Dart">}}
await TruoraValidationsSDK.initialize(InitializeConfig(
     getApiKeyFromSecureStorage: () => getApiKeyFromSecureStorage(),
     userId: userId, // <-- The unique identifier for the user 
     validation: { /* ... */ },
     uiConfig: { /* ... */ },
     language: "es" // <-- Sets the UI language to Spanish
   ));
{{</code>}}

## **Face Validation Configuration**

The FaceValidationConfig allows you to customize face capture and validation behavior:

{{<code lang="dart" lang_title="Dart">}}
final config = FaceValidationConfig(
  // Capture settings
  useAutocapture: true,   // Auto-capture when face is detected
  similarityThreshold: 0.8,  // Matching strictness (0.0 - 1.0)
  timeout: 60,               // Timeout in seconds

  // Result handling
  waitForResults: true, // Wait for API response before closing

  // Optional: Reference face for comparison if not provided
  // one associated with the user id on Truora is used if any.
  referenceFace: ReferenceFace(
    url: 'https://example.com/reference.jpg',
  ),
);
{{</code>}}

### Configuration Parameters:

{{<table "table w-auto small m-auto text-center table-striped table-bordered">}}
| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| **useAutocapture** | bool | true | Automatically capture when face is properly detected |
| **similarityThreshold** | double | 0.8 | Minimum similarity score required (0.0 - 1.0) |
| **timeout** | int | 60 | Maximum time in seconds for the validation |
| **waitForResults** | bool | false | Wait for API response before closing the view |
| **referenceFace** | ReferenceFace | null | Optional reference image for comparison |
{{</table>}}

## **Document Validation Configuration**

The DocumentValidationConfig allows you to customize document capture:

{{<code lang="dart" lang_title="Dart">}}

final config = DocumentValidationConfig(
  // Document details (optional - if not set, a selection screen will be shown)
  country: Country.co,                    // Country code (e.g., co, mx, pe)
  documentType: DocumentType.nationalId,  // Type of document

  // Capture settings
  useAutocapture: true,  // Auto-capture when document is detected
  timeout: 90,           // Timeout in seconds

  // Result handling
  waitForResults: true // Wait for API response before closing
);
{{</code>}}

### Configuration Parameters:

{{<table "table w-auto small m-auto text-center table-striped table-bordered">}}
| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| **country** | Country | null | Country code for document validation (if not set, user selects) |
| **documentType** | DocumentType | null | Type of document (if not set, user selects) |
| **useAutocapture** | bool | true | Automatically capture when document is detected |
| **timeout** | int | 90 | Maximum time in seconds for the validation |
| **waitForResults** | bool | false | Wait for API response before closing the view |
{{</table>}}

### Available Countries and Document Types:

{{<table "table w-auto small m-auto text-center table-striped table-bordered">}}
| Country | Code | Supported Document Types |
| --- | --- | --- |
| Argentina | Country.ar | nationalId |
| Brazil | Country.br | cnh, generalRegistration |
| Chile | Country.cl | nationalId, foreignId, driverLicense, passport |
| Colombia | Country.co | nationalId, foreignId, rut, ppt, passport, identityCard, temporaryNationalId |
| Costa Rica | Country.cr | nationalId, foreignId |
| El Salvador | Country.sv | nationalId, foreignId, passport |
| Mexico | Country.mx | nationalId, foreignId, passport |
| Peru | Country.pe | nationalId, foreignId |
| Venezuela | Country.ve | nationalId |
| All | Country.all | passport |
{{</table>}}

# **Understanding SDK Results**

Before implementing validations, it's important to understand what results you'll receive and how to handle them.

The SDK returns a TruoraValidationResult which can be:

* **completed**: Validation process finished (contains ValidationResult)  
* **canceled:** Validation process canceled by the user (could contain ValidationResult)  
* **error**: SDK error occurred (contains TruoraError)

## **Validation Result Object**

The plugin returns ValidationResult objects with the following structure:

{{<code lang="dart" lang_title="Dart">}}
class ValidationResult {
  final ValidationStatus status;     // Status of the validation
  final String? validationId;        // Unique ID for this validation
  final String? type;                // Type of the validation
}
{{</code>}}

## **Validation Statuses**

{{<table "table w-auto small m-auto text-center table-striped table-bordered">}}
| Status | Description | When it occurs |
| --- | --- | --- |
| **ValidationStatus.success** | Validation passed | User successfully passed the validation |
| **ValidationStatus.failure** | Validation failed | User did not pass the validation criteria |
| **ValidationStatus.pending** | Awaiting processing | Validation is being process |
{{</table>}}

## **TruoraException Types**

When the SDK returns **failed**, the error can be one of three types:

{{<table "table w-auto small m-auto text-center table-striped table-bordered">}}
| Exception Type | Description |
| --- | --- |
| **TruoraException.sdk** | Internal SDK error (configuration, permissions, user actions) |
| **TruoraException.validationApi** | Error from the Truora Validation API |
| **TruoraException.network** | Network connectivity error |
{{</table>}}

Common SDK Error Types (SDKErrorType):

{{<table "table w-auto small m-auto text-center table-striped table-bordered">}}
| Error Type | Code | Description |
| --- | --- | --- |
| **cameraPermissionError** | 20011 | Camera permission was denied |
| **invalidApiKey** | 20017 | API key is invalid or expired |
| **invalidConfiguration** | 20024 | SDK configuration is invalid |
| **networkError** | 20025 | Network connection failed |
| **uploadFailed** | 20026 | Failed to upload captured media |
{{</table>}}

## **Handling Example**

{{<code lang="dart" lang_title="Dart">}}
switch (result) {
  case TruoraValidationCompleted(result: final validation):
   // Validation process completed - check the validation status
   print('Validation ID: ${validation.validationId}');
   print('Status: ${validation.status}');
   print('Type: ${validation.type}');

   if (validation.status == ValidationStatus.success) {
     // User passed the validation
     Navigator.pushNamed(context, '/success');
   } else if (validation.status == ValidationStatus.failure) {
     // User did not pass the validation criteria
     _showRetryDialog();
   }
   break;

  case TruoraValidationError(error: final err, partialResult: final pr):
   // SDK error occurred - handle based on error type
   _handleSDKError(err);
   break;
  case TruoraValidationCanceled(partialResult: final validation):
  // User cancelled - just go back
   Navigator.pop(context);
   break;
}

void _handleSDKError(TruoraException error) {
 switch (error) {
  case TruoraSDKException(code: final code, message: final msg):
     switch (code) {
       case 20011:
         // Camera permission denied 
         _showPermissionDialog();
         break;
       default:
         _showErrorDialog(message);
     }
     break;

  case TruoraNetworkException(message: final msg):
     _showErrorDialog('Connection error: $msg');
     break;

  case TruoraApiException(message: final msg, httpCode: final httpCode):
     _showErrorDialog('API error: ${msg} for http ${httpCode}');
     break;
 }
}
{{</code>}}

# **Implementation**

Now that you understand configurations and result handling, let's implement validations.

## **Basic Face Validation**

Here's a complete example of implementing face validation in Flutter:

{{<code lang="dart" lang_title="Dart">}}
import 'package:flutter/material.dart';
import 'package:truora_validations_sdk/truora_validations_sdk.dart';

class ValidationScreen extends StatefulWidget {
 @override
 _ValidationScreenState createState() => _ValidationScreenState();
}

class _ValidationScreenState extends State<ValidationScreen> {
 
 final SecureApiKeyProvider _apiKeyProvider = SecureApiKeyProvider('YOUR_API_KEY');

 String _validationStatus = 'Ready to start validation';
 bool _isValidating = false;

 @override
 void initState() {
   super.initState();
   _initializeSDK();
 }

 Future<void> _initializeSDK() async {
   try {
     final apiKey = await _apiKeyProvider.getApiKeyFromSecureStorage();
     await TruoraValidationsSDK.initialize(apiKey: apiKey, userId: 'flutter-test-user');
   } catch (e) {
     setState(() {
       _validationStatus = 'Initialization failed: $e';
     });
   }
 }

 Future<void> _startFaceValidation() async {
   setState(() {
     _isValidating = true;
     _validationStatus = 'Starting face validation...';
   });

   // Configure the validation
   final config = FaceValidationConfig(
     useAutocapture: true,
     similarityThreshold: 0.8,
     timeout: 60,
     waitForResults: true,
   );

   // Optional: Customize UI
   final uiConfig = UIConfig(
     primaryColor: '#435AE0',
     surfaceColor: '#FFFFFF',
     errorColor: '#FF5454',
     logoUrl: 'https://your-cdn.com/logo.png',
   );

   // Start the validation
   
  await TruoraValidationsSDK.initialize(InitializeConfig(
     getApiKeyFromSecureStorage: () => _apiKeyProvider.getApiKeyFromSecureStorage(),
     userId: 'flutter-test-user',
     validation: config,
     uiConfig: uiConfig,
     language: 'es' 
   ));
   final result = await TruoraValidationsSDK.start();

   // Handle the result
   _handleValidationResult(result);
 }

 void _handleValidationResult(TruoraValidationResult result) {
   setState(() {
     _isValidating = false;
   });

   switch (result) {
     case TruoraValidationCompleted(result: final validation):
       switch (validation.status) {
         case ValidationStatus.success:
           setState(() {
             _validationStatus = 'Success! ID: ${validation.validationId}';
           });
           break;
         case ValidationStatus.failure:
           setState(() {
             _validationStatus = 'Validation failed. Please try again.';
           });
           break;
         case ValidationStatus.pending:
           setState(() {
             _validationStatus = 'Processing...';
           });
           break;
       }
       break;

     case TruoraValidationError(error: final err, partialResult: final pr):
       setState(() {
         _validationStatus = 'Error: ${err.message}';
       });
       break;
     case TruoraValidationCanceled(partialResult: final validation):
       break;
   }
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text('Truora Validation'),
     ),
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
           Text(
             _validationStatus,
             style: Theme.of(context).textTheme.titleMedium,
             textAlign: TextAlign.center,
           ),
           SizedBox(height: 40),
           ElevatedButton(
             onPressed: _isValidating ? null : _startFaceValidation,
             child: _isValidating
                 ? CircularProgressIndicator(color: Colors.white)
                 : Text('Start Face Validation'),
             style: ElevatedButton.styleFrom(
               padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
             ),
           ),
         ],
       ),
     ),
   );
 }
}
{{</code>}}

## **Document Validation**

For document validation, use the DocumentValidationConfig:

{{<code lang="dart" lang_title="Dart">}}
Future<void> _startDocumentValidation() async {
 final config = DocumentValidationConfig(
   country: Country.co,
   documentType: DocumentType.nationalId,
   useAutocapture: true,
   timeout: 60,
   waitForResults: true,
 );

 await TruoraValidationsSDK.initialize(InitializeConfig(
     getApiKeyFromSecureStorage: () => _apiKeyProvider.getApiKeyFromSecureStorage(),
     userId: 'flutter-test-user',
     validation: config,
     uiConfig: uiConfig,
     language: 'es' 
   ));
 final result = await TruoraValidationsSDK.start();

 _handleValidationResult(result);
}
{{</code>}}

# **Troubleshooting**

Common Issues

1. **Camera not working on Android**  
   Solution: Verify that camera permissions are added to AndroidManifest.xml and that the user has granted permission at runtime.

2. **Build fails on iOS**  
   Solution: Run cd ios && pod install and ensure your iOS deployment target is set to 13.0 or higher in both Podfile and Xcode project settings.

3. **"API Key Invalid" error**  
   Solution: Verify your API key is correct and has the proper grants (generator or sdk type). Check that it hasn't expired.

4. **Validation timeout**  
   Solution: Increase the timeout value in the config, or ensure the device has a stable internet connection.

5. **SDK won't start on Android (`ValidationHandler not available`)**  
   Solution: Your host activity must extend `FlutterFragmentActivity`. The default `FlutterActivity` is not compatible with the native Android SDK. Update `MainActivity.kt` to `class MainActivity : FlutterFragmentActivity()` (see [Android Setup: FlutterFragmentActivity](#android-setup-flutterfragmentactivity-required)).