Document + Face combined validation flow
This guide explains how to run Document validation and Face validation as one seamless flow. The front image of the captured document can be extracted and used as the reference for the subsequent face (liveness) validation.
Overview
The Truora SDK supports running Document validation and Face validation as a single, seamless user experience. Instead of two independent sessions, the two validations share one userId. The front image of the captured document is extracted by Truora and used as the biometric reference for the subsequent face (liveness) check. This eliminates the need to supply a separate reference photo and ensures the liveness comparison is always made against the live document.
How it works
The flow consists of three sequential steps that your app orchestrates:
-
Run a document validation with
waitForResults: trueand afinishViewConfigurationthat hides the finish screen on success but shows it on failure. When the document validation succeeds, the SDK dismisses its finish view and the user continues to the next validation. -
Extract the face reference from the completed document result by calling
getFaceReferenceImage(). This returns the URL of the front image captured during document scanning. -
Run a face validation using the same
userIdand passing the extracted image asreferenceFace. The SDK uses this image as the enrollment reference for the liveness comparison.
At the end of both steps your app holds two independent ValidationResult objects — one for the document, one for the face — both linked to the same userId.
Flow Diagram
[Doc and face flow diagram][/images/illustrations/validations_sdk/doc_and_face_flow_diagram.png]
Requirements and Constraints
finishViewConfiguration and waitForResults interaction
Setting finishViewConfiguration implicitly enables result waiting — you do not need to explicitly set waitForResults: true alongside it. However, if you explicitly set waitForResults: false while also providing a finishViewConfiguration, the SDK treats this as a misconfiguration and returns a TruoraValidationError with code 20024 (invalidConfiguration). Setting waitForResults: true forces the SDK to show a finish screen for all validation statuses, you need to set the finishViewConfiguration explicitly to hide/show certain results
Both steps must share the same userId
Use the same userId for the document validation and the face validation. The Truora backend links the two validations under the same enrollment session using this identifier.
Relation with api key
The same sdk api key can be used for both validations, provided that they must have the same userId associated with it.
Configuration summary
| Field | Document step | Face step |
|---|---|---|
waitForResults |
true required — set explicitly or implied by finishViewConfiguration |
your choice |
finishViewConfiguration |
success: hide, failure: show — implicitly enables waitForResults |
not needed |
referenceFace |
— | URL from getFaceReferenceImage() |
userId |
your user’s ID | same user ID |
Examples
Android — Java
package com.example;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import com.truora.core.TruoraSDK;
import com.truora.core.shared.client.ValidationHandler;
import com.truora.errors.TruoraException;
import com.truora.interfaces.DocumentValidationConfigFunction;
import com.truora.interfaces.FaceValidationConfigFunction;
import com.truora.interfaces.TruoraAPIKeyGetter;
import com.truora.shared.models.FinishViewAction;
import com.truora.shared.models.FinishViewConfiguration;
import com.truora.shared.models.ReferenceFace;
import com.truora.shared.models.TruoraValidationCanceled;
import com.truora.shared.models.TruoraValidationCompleted;
import com.truora.shared.models.TruoraValidationError;
import com.truora.shared.models.ValidationResult;
import java.net.URL;
public class CombinedValidationActivity extends AppCompatActivity implements TruoraAPIKeyGetter {
private static final String TAG = "CombinedValidation";
private static final String USER_ID = "user-unique-id"; // Your persistent user ID
// Register the launcher before onCreate
private final ValidationHandler validationHandler = ValidationHandler.create(this);
@Override
public String getApiKeyFromSecureStorage() {
// Retrieve from secure storage in production
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startDocumentWithFaceFlow();
}
private void startDocumentWithFaceFlow() {
// Step 1: Document Validation
// waitForResults is required so getFaceReferenceImage() is populated.
// On success the finish screen is suppressed so the user flows directly
// into face capture without an intermediate result screen.
FinishViewConfiguration docFinishConfig =
FinishViewConfiguration.of(FinishViewAction.HIDE, FinishViewAction.SHOW);
try {
TruoraSDK.Validations.Builder docBuilder =
TruoraSDK.Validations.Builder(this, USER_ID);
docBuilder.withValidation((DocumentValidationConfigFunction) config ->
config.useAutocapture(true)
.waitForResults(true)
.withFinishViewConfiguration(docFinishConfig)
.build()
);
docBuilder.build(validationHandler);
validationHandler.start(docResult -> {
if (docResult instanceof TruoraValidationCompleted<?> completed) {
ValidationResult documentResult = (ValidationResult) completed.getValue();
Log.i(TAG, "Document validation completed: " + documentResult.getId());
startFaceValidation(documentResult);
} else if (docResult instanceof TruoraValidationError<?> error) {
Log.e(TAG, "Document validation error: " + error.getFailureMessage());
} else if (docResult instanceof TruoraValidationCanceled<?>) {
Log.i(TAG, "Document validation canceled");
}
});
} catch (TruoraException | IllegalArgumentException e) {
Log.e(TAG, "Failed to start document validation", e);
}
}
private void startFaceValidation(ValidationResult documentResult) {
// Step 2: Extract face reference
URL faceImageUrl = documentResult.getFaceReferenceImage();
if (faceImageUrl == null) {
Log.w(TAG, "No face reference image available from document result");
return;
}
ReferenceFace referenceFace;
try {
referenceFace = ReferenceFace.from(faceImageUrl);
} catch (TruoraException e) {
Log.e(TAG, "Failed to build ReferenceFace", e);
return;
}
// Step 3: Face Validation
// Use the same USER_ID to link this validation to the document session.
try {
TruoraSDK.Validations.Builder faceBuilder =
TruoraSDK.Validations.Builder(this, USER_ID);
final ReferenceFace ref = referenceFace;
faceBuilder.withValidation((FaceValidationConfigFunction) config ->
config.useAutocapture(true)
.waitForResults(true)
.useReferenceFace(ref)
.build()
);
faceBuilder.build(validationHandler);
validationHandler.start(faceResult -> {
if (faceResult instanceof TruoraValidationCompleted<?> completed) {
ValidationResult faceValidationResult = (ValidationResult) completed.getValue();
Log.i(TAG, "Face validation completed: " + faceValidationResult.getId()
+ " status=" + faceValidationResult.getStatus());
onBothValidationsComplete(documentResult, faceValidationResult);
} else if (faceResult instanceof TruoraValidationError<?> error) {
Log.e(TAG, "Face validation error: " + error.getFailureMessage());
} else if (faceResult instanceof TruoraValidationCanceled<?>) {
Log.i(TAG, "Face validation canceled");
}
});
} catch (TruoraException | IllegalArgumentException e) {
Log.e(TAG, "Failed to start face validation", e);
}
}
private void onBothValidationsComplete(ValidationResult document, ValidationResult face) {
Log.i(TAG, "Both validations complete."
+ " Document: " + document.getStatus()
+ " Face: " + face.getStatus());
// Navigate to your results screen or handle as needed
}
}
Android — Kotlin
package com.example
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.truora.core.TruoraSDK
import com.truora.core.shared.client.ValidationHandler
import com.truora.errors.TruoraException
import com.truora.interfaces.TruoraAPIKeyGetter
import com.truora.shared.models.FinishViewAction
import com.truora.shared.models.FinishViewConfiguration
import com.truora.shared.models.ReferenceFace
import com.truora.shared.models.TruoraValidationCanceled
import com.truora.shared.models.TruoraValidationCompleted
import com.truora.shared.models.TruoraValidationError
import com.truora.shared.models.ValidationResult
class CombinedValidationActivity : AppCompatActivity(), TruoraAPIKeyGetter {
companion object {
private const val TAG = "CombinedValidation"
private const val USER_ID = "user-unique-id" // Your persistent user ID
}
// Register the launcher before onCreate
private val validationHandler = ValidationHandler.create(this)
override fun getApiKeyFromSecureStorage(): String {
// Retrieve from secure storage in production
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startDocumentWithFaceFlow()
}
private fun startDocumentWithFaceFlow() {
// Step 1: Document Validation
// waitForResults is required so getFaceReferenceImage() is populated.
// On success the finish screen is suppressed so the user flows directly
// into face capture without an intermediate result screen.
val docFinishConfig = FinishViewConfiguration.of(FinishViewAction.HIDE, FinishViewAction.SHOW)
try {
TruoraSDK.Validations
.Builder(this, USER_ID)
.withValidation { config ->
config.useAutocapture(true)
.waitForResults(true)
.withFinishViewConfiguration(docFinishConfig)
.build()
}
.build(validationHandler)
validationHandler.start { docResult ->
when (docResult) {
is TruoraValidationCompleted<*> -> {
val documentResult = docResult.value as ValidationResult
Log.i(TAG, "Document validation completed: ${documentResult.id}")
startFaceValidation(documentResult)
}
is TruoraValidationError<*> ->
Log.e(TAG, "Document validation error: ${docResult.failureMessage}")
is TruoraValidationCanceled<*> ->
Log.i(TAG, "Document validation canceled")
}
}
} catch (e: TruoraException) {
Log.e(TAG, "Failed to start document validation", e)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Failed to start document validation", e)
}
}
private fun startFaceValidation(documentResult: ValidationResult) {
// ── Step 2: Extract face reference
val faceImageUrl = documentResult.getFaceReferenceImage() ?: run {
Log.w(TAG, "No face reference image available from document result")
return
}
val referenceFace = try {
ReferenceFace.from(faceImageUrl)
} catch (e: TruoraException) {
Log.e(TAG, "Failed to build ReferenceFace", e)
return
}
// Step 3: Face Validation
// Use the same USER_ID to link this validation to the document session.
try {
TruoraSDK.Validations
.Builder(this, USER_ID)
.withValidation { config ->
config.useAutocapture(true)
.waitForResults(true)
.useReferenceFace(referenceFace)
.build()
}
.build(validationHandler)
validationHandler.start { faceResult ->
when (faceResult) {
is TruoraValidationCompleted<*> -> {
val faceValidationResult = faceResult.value as ValidationResult
Log.i(TAG, "Face validation completed: ${faceValidationResult.id} " +
"status=${faceValidationResult.status}")
onBothValidationsComplete(documentResult, faceValidationResult)
}
is TruoraValidationError<*> ->
Log.e(TAG, "Face validation error: ${faceResult.failureMessage}")
is TruoraValidationCanceled<*> ->
Log.i(TAG, "Face validation canceled")
}
}
} catch (e: TruoraException) {
Log.e(TAG, "Failed to start face validation", e)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Failed to start face validation", e)
}
}
private fun onBothValidationsComplete(document: ValidationResult, face: ValidationResult) {
Log.i(TAG, "Both validations complete. Document: ${document.status} Face: ${face.status}")
// Navigate to your results screen or handle as needed
}
}
iOS — Swift
import UIKit
import TruoraValidationsSDK
// MARK: - API Key Provider
private final class ApiKeyProvider: TruoraAPIKeyGetter {
private let key: String
init(_ key: String) { self.key = key }
func getApiKeyFromSecureLocation() async throws -> String { key }
}
// MARK: - Combined Validation View Controller
class CombinedValidationViewController: UIViewController {
private let userId = "user-unique-id" // Your persistent user ID
private let apiKeyProvider = ApiKeyProvider("YOUR_SDK_API_KEY")
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Task { await startDocumentWithFaceFlow() }
}
private func startDocumentWithFaceFlow() async {
// Step 1: Document Validation
// waitForResults is required so getFaceReferenceImage() is populated.
// On success the finish screen is suppressed so the user flows directly
// into face capture without an intermediate result screen.
let docValidation = TruoraValidationsSDK.Builder(
apiKeyGenerator: apiKeyProvider,
userId: userId
)
.withValidation { (doc: Document) in
doc.useAutocapture(true)
.waitForResults(true)
.setFinishViewConfiguration(
FinishViewConfiguration(success: .hide, failure: .show)
)
}
.build()
let docResult: TruoraValidationResult<ValidationResult> =
await withCheckedContinuation { continuation in
Task { @MainActor in
await docValidation.start(from: self) { continuation.resume(returning: $0) }
}
}
guard case .completed(let documentResult) = docResult else {
// Failure or cancellation — the SDK already handled the finish screen
// for the failure case; just return here.
return
}
print("Document validation completed: \(documentResult.validationId)")
// Step 2: Extract face reference
guard let faceImageUrl = documentResult.getFaceReferenceImage() else {
print("No face reference image available from document result")
return
}
let referenceFace: ReferenceFace
do {
referenceFace = try ReferenceFace.from(faceImageUrl)
} catch {
print("Failed to build ReferenceFace: \(error.localizedDescription)")
return
}
// Step 3: Face Validation
// Use the same userId to link this validation to the document session.
let faceValidation = TruoraValidationsSDK.Builder(
apiKeyGenerator: apiKeyProvider,
userId: userId
)
.withValidation { (face: Face) in
face.useAutocapture(true)
.waitForResults(true)
.useReferenceFace(referenceFace)
}
.build()
await faceValidation.start(from: self) { [weak self] faceResult in
switch faceResult {
case .completed(let faceValidationResult):
print("Face validation completed: \(faceValidationResult.validationId) "
+ "status=\(faceValidationResult.status.rawValue)")
self?.onBothValidationsComplete(
document: documentResult,
face: faceValidationResult
)
case .error(let error):
print("Face validation error: \(error.localizedDescription)")
case .canceled:
print("Face validation canceled")
}
}
}
private func onBothValidationsComplete(
document: ValidationResult,
face: ValidationResult
) {
print("Both validations complete. Document: \(document.status) Face: \(face.status)")
// Navigate to your results screen or handle as needed
}
}
Flutter — Dart
import 'package:flutter/material.dart';
import 'package:flutter_validations_sdk/flutter_validations_sdk.dart';
class CombinedValidationScreen extends StatefulWidget {
const CombinedValidationScreen({super.key});
@override
State<CombinedValidationScreen> createState() =>
_CombinedValidationScreenState();
}
class _CombinedValidationScreenState
extends State<CombinedValidationScreen> {
static const _userId = 'user-unique-id'; // Your persistent user ID
String _status = 'Ready';
bool _isRunning = false;
@override
void initState() {
super.initState();
_startDocumentWithFaceFlow();
}
Future<String> _getApiKey() async => // Retrieve from secure storage;
Future<void> _startDocumentWithFaceFlow() async {
setState(() {
_isRunning = true;
_status = 'Starting document validation…';
});
// Step 1: Document Validation
// waitForResults is required so getFaceReferenceImage() is populated.
// On success the finish screen is suppressed so the user flows directly
// into face capture without an intermediate result screen.
await TruoraValidationsSDK.initialize(InitializeConfig(
getApiKeyFromSecureStorage: _getApiKey,
userId: _userId,
validation: DocumentConfig(DocumentValidationConfig(
useAutocapture: true,
waitForResults: true,
finishViewConfiguration: PigeonFinishViewConfiguration(
success: PigeonFinishViewVisibility.hide,
failure: PigeonFinishViewVisibility.show,
),
timeout: 120,
)),
));
final docResult = await TruoraValidationsSDK.start();
if (docResult is! TruoraValidationCompleted) {
// Failure or cancellation — the SDK already handled the finish screen
// for the failure case; update state and return.
setState(() {
_isRunning = false;
_status = 'Document validation did not complete';
});
return;
}
debugPrint('Document validation completed: ${docResult.result.id}');
// Step 2: Extract face reference
final faceRef = docResult.result.getFaceReferenceImage();
if (faceRef == null || faceRef.isEmpty) {
setState(() {
_isRunning = false;
_status = 'No face reference image available from document result';
});
return;
}
setState(() => _status = 'Starting face validation…');
// Step 3: Face Validation
// Use the same _userId to link this validation to the document session.
await TruoraValidationsSDK.initialize(InitializeConfig(
getApiKeyFromSecureStorage: _getApiKey,
userId: _userId,
validation: FaceConfig(FaceValidationConfig(
referenceFace: faceRef,
useAutocapture: true,
waitForResults: true,
similarityThreshold: 0.8,
timeout: 120,
)),
));
final faceResult = await TruoraValidationsSDK.start();
setState(() {
_isRunning = false;
_status = 'Done';
});
_onBothValidationsComplete(docResult, faceResult);
}
void _onBothValidationsComplete(
TruoraValidationCompleted docResult,
TruoraValidationResult faceResult,
) {
final faceStatus = faceResult is TruoraValidationCompleted
? faceResult.result.status.name
: 'not completed';
debugPrint(
'Both validations complete. '
'Document: ${docResult.result.status.name} '
'Face: $faceStatus',
);
// Navigate to your results screen or handle as needed
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _isRunning
? const CircularProgressIndicator()
: Text(_status),
),
);
}
}
React Native — TypeScript
import React, { useEffect } from 'react';
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
import TruoraSDK from '@truora/validations-sdk';
const USER_ID = 'user-unique-id'; // Your persistent user ID
export default function CombinedValidationScreen() {
useEffect(() => {
startDocumentWithFaceFlow();
}, []);
async function startDocumentWithFaceFlow() {
// Step 1: Document Validation
// waitForResults is required so getFaceReferenceImage() is populated.
// On success the finish screen is suppressed so the user flows directly
// into face capture without an intermediate result screen.
await TruoraSDK.initialize({
getApiKeyFromSecureStorage: () => // Retrieve from secure storage,
userId: USER_ID,
validation: {
type: 'document',
useAutocapture: true,
waitForResults: true,
finishViewConfiguration: {
success: 'hide',
failure: 'show',
},
timeout: 120,
},
});
const docResult = await TruoraSDK.start();
if (docResult.type !== 'completed') {
// Failure or cancellation — the SDK already handled the finish screen
// for the failure case; return here.
return;
}
console.log('Document validation completed:', docResult.validation.validationId);
// Step 2: Extract face reference
const faceReference = docResult.validation.getFaceReferenceImage();
if (!faceReference) {
console.warn('No face reference image available from document result');
return;
}
// Step 3: Face Validation
// Use the same USER_ID to link this validation to the document session.
await TruoraSDK.initialize({
getApiKeyFromSecureStorage: () => // Retrieve from secure storage,
userId: USER_ID,
validation: {
type: 'face',
referenceFace: faceReference,
useAutocapture: true,
waitForResults: true,
similarityThreshold: 0.8,
timeout: 120,
},
});
const faceResult = await TruoraSDK.start();
console.log('Face validation completed:', faceResult.validation?.validationId);
onBothValidationsComplete(docResult.validation, faceResult.validation);
}
function onBothValidationsComplete(document: any, face: any) {
console.log(
'Both validations complete.',
'Document:', document?.status,
'Face:', face?.status,
);
// Navigate to your results screen or handle as needed
}
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
<Text style={styles.text}>Running validations…</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
text: { marginTop: 16, fontSize: 16 },
});