Only this pageAll pages
Powered by GitBook
1 of 35

Mobile SDK

Loading...

Introduction

Loading...

Loading...

Loading...

Mobile SDK Guide

Loading...

Loading...

Loading...

Loading...

Loading...

Mobile SDK Reference

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Mobile SDK Use Cases

Loading...

Loading...

Loading...

Mobile SDK Changelog

Loading...

Server API

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Keyless SDK Documentation

Nothing to remember. Nothing to steal. You are the key.

Key steps embed privacy-preserving biometric authentication in your mobile applications:

Step 1: Understand how the Keyless components interact with your application server.

Step 2: Understand a typical biometric authentication flow and how it interacts with your application.

Step 3: Follow the for Android or iOS.

getting started guide

Integration Flows

Learn how the Keyless components interact with your app.

Introduction

Learn how the Keyless SDK components can be integrated into a mobile application and backend server, to enable biometric authentication.

Integration Overview

Enrollment Flow

To authenticate with Keyless, a user must first enroll their biometric template. Enrollment with Keyless consists of registering the user’s biometric features in a privacy-preserving manner using the various from the Keyless SDK.

Authentication Flow

The most common authentications scenarios for the Keyless SDK are:

  • access to a web application

  • access on a mobile application

Authentication in a web application

In this scenario the user is trying access to a resource in a web application for which strong authentication is required. The web application backend sends a push notification to the customer app to request that the user identify themselves with Keyless. After biometric authentication is successful, the flow returns to the web application backend, which leverages the APIs exposed by the Keyless backend to perform additional security checks.

Once the Keyless backend confirms that the authentication was successful, the user is allowed access to the resource.

Authentication on a mobile app

In this scenario the user is trying access to a resource directly in the mobile application for which strong authentication is required. The mobile application sends a push notification to the customer app to request that the user identify themselves with Keyless. After biometric authentication is successful, the flow returns to the mobile application backend, which leverages the APIs exposed by the Keyless backend to perform additional security checks.

Once the Keyless backend confirms that the authentication was successful, the user is allowed access to the resource.

Keyless Components

As mentioned, Keyless is composed of two main blocks:

  • Keyless SDK

  • Keyless backend / Confirmation API Service

Keyless SDK

The Keyless SDK supports both Android and iOS, and exposes API methods to interact with the Keyless Privacy-Preserving Network to perform the following actions:

  • Enroll a user

  • Authenticate

  • De-Enroll

  • Restore backup

Keyless backend

The Keyless SaaS backend offers APIs which can be used to perform security checks through Backend-to-Backend calls. Specifically, it is possible to interrogate the Keyless Backend after the SDK returns an OK response for an Authentication attempt.

Components

Learn about the interaction between Keyless components

The diagrams below show how the Keyless SDK, which runs within your mobile app on the user’s device, interacts with your application server and with the Keyless network.

Enrollment

During enrollment, your mobile app invokes the enroll method from the Keyless SDK, and then:

  1. Guides the user through capturing a biometric signal with the device camera.

  2. Interacts with Keyless to generate a new user identifier (Keyless ID), which is then returned to your mobile app.

Authentication

Authentication involves your application server, your mobile app, and the Keyless network, as depicted in Figure 2:

  1. This process starts when the user performs an action that requires authentication using your mobile app.

  2. The app provides the details of this action to your application server, which generates a challenge. The challenge is sent to the mobile app, which uses the Keyless SDK to compute the corresponding authentication token using the authenticate method.

  3. The Keyless SDK authenticates the user by capturing the user’s biometrics using the mobile device’s camera.

Account Deletion

Account deletion is similar to authentication. First, your mobile app performs steps 1-6, then it notifies your application server that the user wants to delete the account. Next, your mobile app invokes the deEnroll method from the Keyless SDK. This method issues a deletion request to the Keyless network (Step 7). The request removes all data associated with the user from the Keyless network.

enrollment methods
The Keyless SDK connects to the Keyless network, and runs a secure multi-party computation protocol that authenticates the user and generates the authentication token in response to the challenge provided in Step 2. The Keyless SDK returns the authentication token to the mobile app.
  • The app sends the token to your application server, which verifies it.

  • If the authentication token is valid, the application server completes the transaction and notifies your mobile app.

  • authentication
    Figure 1: Keyless enrollment diagram
    Figure 2: Keyless authentication diagram
    Figure 3: Keyless de-enrollment diagram

    Getting Started

    This API allows for backend-to-backend communication between your backend and the Keyless servers.

    The server API has end points for automating aspects of your Keyless setup:

    • List and revoke devices

    • Remove users

    • Perform extra backend to backend security operations

    API Access

    • All API endpoints require the X-Api-Key: <SECRET_API_KEY> header with your security key

    • All API endpoints are available at https://api.keyless.io

    Telemetry

    This API calls allows you to retrieve meta data related to a user or specific transaction.

    Introduction

    Keyless is inevitably used as part of a wider authentication flow, whether it's an account recovery, login or step-up authentication. Integrators use this feature to collect additional insights around user transactions such as the result, errors and type of authentication and then leverage this data in their own systems for analytics, orchestration or fraud decisioning.

    Telemetry data can be retrieved for specific transactions using the operationID. This ID is generated via the Mobile SDK by passing an operationInfo object to the EnrollConfig or AuthConfig following a specific transaction. This API also allows customers to search via the integrators External User ID i.e. the unique identifier the user is known by in your own system. For details on how to create and manage the mapping from External User ID to the Keyless ID head .

    Introduce Keyless to Users

    In order to introduce Keyless to users before the very first enrollment, it may be helpful to show an explanatory screen on what is the flow about to start.

    This screen is showable via the public API Keyless.showIntroductionScreen. This function can be called at any moment (even before setup). It expects a completion for when the user taps on the main CTA. The message can be customized as explained in UI-Customization

    Examples

    Keyless.showIntroductionScreen { 
        // Perform actions after user taps CTA
    }
    Keyless.showIntroductionScreen { 
        // Perform actions after user taps CTA
    }

    Sample Apps

    A description of, and links to, the Keyless sample apps to support integrators with a working example.

    Keyless has created sample apps in both Android and iOS as companions to the official documentation. This effectively shows our SDKs working inside a basic application that can perform both and flows.

    This supports:

    • Developers who want to integrate and use the Keyless Mobile SDK in their apps, by allowing them to see the SDK functioning inside a working app or emulator.

    • A working demonstration of our SDK to see it in action.

    The below links will launch the relevant github pages in a new tab:

    Devices

    This set of API calls allows you to fetch and manipulate Keyless enabled devices.

    Operations

    This set of API calls allows you to fetch operations on various Keyless entities.

    Operations

    JWT

    Account recovery

    This page explains what we mean by account recovery and the context you may need to integrate it.

    What is Account Recovery?

    Account Recovery is the term we use to describe the use case where a user is known to our customer (i.e. is registered in Keyless), but needs to be authenticated on a new device. Typically this is either where:

    • They had previously been enrolled on a device but the user no longer has access to it.

    Users

    This set of API calls allows you to make operations on various Keyless users.

    External User ID

    This set of API calls allows you to perform operations to Create, Modify, Delete and Retrieve External User IDs

    Introduction

    An external user is your way to set a custom username in Keyless, allowing you to represent Keyless users with an identifier that is more familiar to your business such as a Unique identifier, GUID or UUID. Customers are explicitly advised not to input pii in this field (i.e. custom usernames, names or email addresses).

    A Keyless user may have zero or one external users bound to itself and the deletion of an external user doesn't imply the Keyless user will be deleted as well.

    Android sample app

  • iOS sample app

  • Enrollment
    Authentication

    They are adding a back-up device.

  • They are known to our customer, perhaps having submitted a selfie during onboarding, but have yet to authenticate on a device via our Mobile SDK within our customer's app (which would have to leverage Keyless IDV Bridge).

  • Client State

    Keyless is able to recover an account from the Keyless client state.

    The client state is obtained either:

    1. From your backend through Keyless IDV Bridge.

      • To understand how to generate this state see our IDV Bridge SaaS or IDV On-Premise

      • Then head to New Device Activation to understand how to leverage this state to bind a userID to a new device.

    2. From your client app using the Keyless Mobile SDK:

      • during live enrollment or authentication.

      • Then head to to understand how to leverage this state to bind a userID to a new device.

    here

    JWT signing

    Keyelss mobile SDK can generate a signed a JWT containing a custom payload. You can use the signed JWT to implement .

    Generate signed JWT

    Pass JwtSigningInfo to the authentication to generate a signed JWT:

    Generate the client state
    New Device Activation

    User signing public key

    The AuthenticationSuccess contains the following fields:

    • signedJwt: the signed JWT.

    Dynamic Linking
    //Keyless adds a td claim to the JWTs containing the data you specify
    val jwtSigningInfo = JwtSigningInfo(claimTransactionData = "<your custom data")
    
    // if you want to authenticate with biometric
    val biomAuthConfig = BiomAuthConfig(jwtSigningInfo = jwtSigningInfo)
    // if you want to authenticate with pin
    val pinAuthConfig = PinAuthConfig(pin = "1234", jwtSigningInfo = jwtSigningInfo)
    
    // perform the authentication
    Keyless.authenticate(
        configuration = biomAuthConfig, // pinAuthConfig if you use pin
        onCompletion = { /*TODO: process result*/ }
    )
    //Keyless adds a td claim to the JWTs containing the data you specify
    let jwtSigningInfo = JwtSigningInfo(claimTransactionData: "<your custom data>")
    
    // if you want to authenticate with biometric
    let biomAuthConfig = BiomAuthConfig(jwtSigningInfo: jwtSigningInfo)
    // if you want to authenticate with pin
    let pinAuthConfig = PinAuthConfig(pin: "1234", jwtSigningInfo: jwtSigningInfo)
    
    // perform the authentication
    Keyless.authenticate(
        configuration: biomAuthConfig, // pinAuthConfig if you use pin
        onCompletion : { /*TODO: process result*/ }
    )
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/jwt_signing_info.dart';
    import 'package:keyless_flutter_sdk/models/configurations/authentication_configuration.dart';
    
    // Keyless adds a td claim to the JWTs containing the data you specify
    final jwtSigningInfo = JwtSigningInfo("<your custom data>");
    
    // if you want to authenticate with biometric
    final biomAuthConfig = BiomAuthConfig(
        jwtSigningInfo: jwtSigningInfo
    );
    
    // if you want to authenticate with pin
    final pinAuthConfig = PinAuthConfig(
        pin: "1234",
        jwtSigningInfo: jwtSigningInfo
    );
    
    try {
        // perform the authentication with either biometric or pin config
        final result = await Keyless.instance.authenticate(
            biomAuthConfig // or pinAuthConfig if you use pin
        );
        print("JWT signed successfully: ${result.signedJwt}");
    } catch (error) {
        print("Authentication failed: $error");
    }
        // Keyless adds a td claim to the JWTs containing the data you specify
        const jwtSigningInfo = new JwtSigningInfo('<your custom data>');
    
        // if you want to authenticate with biometric
        const biomAuthConfig = new BiomAuthConfig({
          jwtSigningInfo: jwtSigningInfo,
        });
        // if you want to authenticate with pin
        const pinAuthConfig = new PinAuthConfig({
          pin: '1234',
          jwtSigningInfo: jwtSigningInfo,
        });
    
        const result = await Keyless.authenticate(
          biomAuthConfig // or pinAuthConfig if you use pin
        );
    
        result.fold({
          onSuccess(data) {
            logConsole('Auth result success ' + JSON.stringify(data, null, 2));
          },
          onFailure(error) {
            logConsole('Auth result failure ' + JSON.stringify(error, null, 2));
          },
        });

    4️⃣ De-Enrollment

    De-enrollment is the biometric equivalent of an account deletion. Keyless performs an authentication to compare the user's facial biometrics with the ones computed during enrollment. If the biometrics match, the user is authenticated and their account will be removed from Keyless. This operation is irreversible.

    val configuration = BiomDeEnrollConfig()
    
    Keyless.deEnroll(
        deEnrollmentConfiguration = configuration,
        onCompletion = { result ->
            when (result) {
                is Keyless.KeylessResult.Success -> Log.d("KeylessSDK "
    
    let configuration = BiomDeEnrollConfig()
        .build()
    
    Keyless.deEnroll(
        deEnrollmentConfiguration: configuration,
        onCompletion: { error in
            if let error = error {
    
    

    DeEnrollment configuration

    Camera Delay

    Use cameraDelaySeconds to specify the delay (in seconds) between when the camera preview appears, and when the liveness processing starts.

    Success Feedback

    Use showSuccessFeedback to show a Success text on top of the screen when the DeEnroll is successful.

    Liveness Settings

    Using livenessConfiguration you can configure the liveness security level during enrollment. The possible liveness configuration are under LivenessSettings.LivenessConfiguration :

    You can also specify a livenessEnvironmentAware that is by default se to true to enhance liveness detection. This parameters helps to ensure the user is in a suitable setting for verification.

    More details on liveness in the dedicated section.

    New Device Activation

    This page explains how to then use the client state to then authenticate the user and bind the new device to then support ongoing 2 factor authentication.

    Recover from client state

    Pass the client state created during the enrollment flow or via IDV Bridge to recover the account on a new device. The client state is the one you obtained and stored securely when enrolling users via IDV Bridge or the Mobile SDK on the previous sub-page

    When enrolling from the client state, Keyless shows the enrollment UI to users as of 5.0.1 including the live filters.

    Generating client state via the Mobile SDK

    This page explains how to create a client state during a Keyless Enrollment or Authentication, which can then be leveraged for binding a user on a new device.

    What is the client state?

    The Keyless client state contains all the necessary information to restore an account. It can be created during enrollment and authentication

    To create and use the client state Keyless requires the user's biometric to be authenticated.

    Lockout Policy

    This pages explains how the Lockout Policy works, what the implications are for users and how it is set.

    From version 5.0.0 of the SDK onwards the lockout policy is configured on the server side and errors will be tracked and count towards the policy regardless of whether they occur on the client or server side. Please contact the Keyless team if you have questions or you would like to request changes to your policy.

    Keyless has both client side (applicable to a specific device) and server side (applicable to all users and devices) lockout policies to help prevent brute force attacks.

    Client side lockout is configurable in the SDK, and determines how many failed login attempts (lockoutAttemptsThreshold) are allowed over a set time period (lockoutAttemptsResetAfter) before the user is locked out for the set duration (lockoutDuration) on that device.

    Account lockoutDuration

    Liveness Settings

    Liveness Configuration

    Keyless SDK provides three officially-supported configurations for the liveness detection (antispoofing component), listed below from the lowest to the highest level of security:

    • PASSIVE_STANDALONE_MEDIUM - for testing purposes only

    Dynamic Linking

    You can leverage the Keyless authentication mechanism to sign unrelated transactions, including Strong customer authentication (SCA) transactions.

    Payment service providers compliant with are required to:

    • Generate an authentication code specific to the amount of the payment transaction and the payee agreed to by the payer when initiating the transaction

    • Make the payer aware of the amount of the payment transaction, and of the payee

    Keyless helps you by:

    Photo Enrollment

    Use a photo (from an identity document) to enroll the initial biometric data of the user

    As introduced in the :

    enrollment is the process of registering a new user by connecting their facial biometrics to a Keyless account. During this process, a full and unobstructed view of the user's face is required.

    If you possess a trusted source, such as an identity document, Keyless allows you to register a new user connecting their facial biometric from the identity document photo. The assumption behind the feature is that the identity document photo fulfills the requirement of a full and unobstructed view of the user's face.

    To retrieve a document photo Keyless offers the utility.

    To enroll a user from the photo use the

    val configuration = DeEnrollmentConfiguration.builder.build()
    
    Keyless.deEnroll(
        deEnrollmentConfiguration = configuration,
        onCompletion = { result ->
            when (result) {
                is Keyless.KeylessResult.Success -> Log.d("KeylessSDK ", "De-enroll success")
                is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "De-enroll failure - error code ${result.error.code}")
            }
        }
    )
    let configuration = Keyless.DeEnrollmentConfiguration.builder
        .build()
    
    Keyless.deEnroll(
        deEnrollmentConfiguration: configuration,
        onCompletion: { error in
            if let error = error {
                  print("De-Enrollment finished with error: \(error.message)")
            } else {
                  print("De-Enrollment finished with success")
            }
        }
    }
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/deenrollment_configuration.dart';
    
    // Biometric de-enrollment
    final configuration = BiomDeEnrollConfig(
        livenessConfiguration: LivenessConfiguration.passiveStandaloneHigh,
        livenessTimeout: 60,
        cameraDelaySeconds: 0
    );
    
    // Or PIN-based de-enrollment
    final configuration = PinDeEnrollConfig(
        pin: "1234"
    );
    
    try {
        await Keyless.instance.deEnroll(configuration);
        print("De-enrollment successful");
    } catch (error) {
        print("De-enrollment failed: $error");
    }
    import Keyless, {
      BiomDeEnrollConfig,
      PinDeEnrollConfig,
      LivenessConfiguration
    } from '@react-native-keyless/sdk';
    
    // Biometric de-enrollment
    const biometricDeEnroll = async () => {
      const config = new BiomDeEnrollConfig();
      const result = await Keyless.deEnroll(config);
      result.fold({
        onSuccess: () => {
          console.log('De-enrollment successful');
        },
        onFailure: (error) => {
          console.error('De-enrollment failed:', error);
        },
      });
    };
    
    // PIN-based de-enrollment
    const pinDeEnroll = async () => {
      const config = new PinDeEnrollConfig('1234');
      const result = await Keyless.deEnroll(config);
      result.fold({
        onSuccess: () => {
          console.log('De-enrollment successful');
        },
        onFailure: (error) => {
          console.error('De-enrollment failed:', error);
        },
      });
    };
    must be greater than or equal to the
    lockoutAttemptsResetAfter
    so that it is not reset by
    lockoutAttemptsResetAfter
    .

    Server side lockout works similarly, except applies to all authentication devices for a specific user, and is configured to lock a user out for 10 minutes after 5 failed attempts. A successful login resets the count of failed authentication attempts to zero.

    Lockout options and defaults

    When a user exceeds a maximum number of failed attempts within a specified tine window, they will be locked out for the duration of the specified time window. This is effectively controlled by three configurable settings, with definitions of each and defaults listed below.

    Lockout configurations
    Description
    Defaults (SaaS customers)

    Max failed attempts

    How many failed authentications a user is allowed before being “locked out” for the defined suspension period

    5

    Time window

    The number of consecutive failed authentication attempts that must occur within X seconds for authentication to be suspended. Note that any successful authentication resets this to zero.

    600 (10 minutes)

    Suspension period

    How long the account will be suspended, given the max failed attempts is exceeded during the defined time window (in seconds).

    600 (10 minutes)

    How it works

    • The policy is applied per Keyless instance, per Keyless ID (representing a single user).

      • For this reason, customers that are leveraging our component interoperability capability (i.e. with users authenticating on both Web and Mobile) should note that a customer's errors, and any subsequent lockouts, will apply to both Web and Mobile

    • We count failed authentications across the given time window (see table above). Any successful authentication before reaching the failed attempt threshold resets the failed attempt count to zero.

    • The lockout policy cannot be disabled. If a non-restrictive behavior is desired, it's recommended to set a high max failed attempts value and/or reduce sensitivity in the time window settings.

    • If you would like to change the settings at any time, get in-touch with a Keyless team member or [email protected]

    When is the lockout policy applied

    • The lockout policy applies to Authentications but is not applied at all to enrollment flows.

    • From SDK v5.3.x and above the lockout policy also applies to Account Recovery leveraging the Enroll from Client State flow.

    • Note in the case of an enrollment failure, the reason the lockout policy cannot be applied is that no Keyless ID has been generated and it is therefore not possible for Keyless to track the relationship between enrollment attempts currently.

    If a user is locked-out

    • Any authentication attempt for that Keyless ID will trigger a 30007 "User Lockout" error.

    • They must wait for the lockout duration to expire. There is no way to override or bypass this lockout.

    • If a user attempts to authenticate while being locked out, the Time Window doesn’t reset even if it’s presented with an error for having reached the maximum number of attempts.

      • In this case the biometric authentication is not attempted at all and circuits are not consumed.

    lockoutDuration: Long,                //seconds - default 300 seconds
    lockoutAttemptsResetAfter: Long,      //seconds - default 180 seconds
    lockoutAttemptsThreshold: Int         //number  - default 5 attempts
    ,
    "De-enroll success"
    )
    is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "De-enroll failure - error code ${result.error.code}")
    }
    }
    )
    print("De-Enrollment finished with error: \(error.message)")
    } else {
    print("De-Enrollment finished with success")
    }
    }
    }
    liveness settings
    PASSIVE_STANDALONE_MEDIUM
    PASSIVE_STANDALONE_HIGH        //recommended configuration
    PASSIVE_STANDALONE_HIGHEST
    
    // clientState retrieved from previous step
    val clientState = "<your_client_state>"
    
    val enrollConfig = BiomEnrollConfig(clientState = clientState)
    
    Keyless.enroll(
      configuration = enrollConfig,
      onCompletion = { result ->
    

    The account is recovered and it's now possible to authenticate the user with ongoing 2 factor authentication with a single

    The temporay state internals are not important but you can expect a string similar to the following that you should pass as-is to recover the account:

    Note that when generating the client state within your client app using the Keyless Mobile SDK a maximum of 50 client states are allowed to be generated. This ensures that the experience remains performant.

    Obtain the client state

    Use the generatingClientState parameter of the BiomEnrollConfig or BiomAuthConfig. When doing so, we recommend choosing BACKUP as the client state type to be generated. Note: if you’re using Flutter, this parameter will instead be named shouldRetrieveTemporaryState.

    Dunring the enrollment flow:

    val enrollConfig = BiomEnrollConfig(generatingClientState = ClientStateType.BACKUP)
    
    Keyless.enroll(
      configuration = enrollConfig,
      onCompletion = { result ->
        when (result) {
          is Keyless.KeylessResult.Success -> {
    
    

    During the authentication flow:

    val authConfig = BiomAuthConfig(generatingClientState = ClientStateType.BACKUP)
    
    Keyless.authenticate(
      configuration = authConfig,
      onCompletion = { result ->
        when (result) {
          is Keyless.KeylessResult.Success -> {
    
    

    During the enrollment flow:

    During the authentication flow:

    During the enrollment flow:

    During the authentication flow:

    During the enrollment flow:

    During the authentication flow:

    PASSIVE_STANDALONE_HIGH - for most production use (current SDK default)

  • PASSIVE_STANDALONE_HIGHEST - for higher security production use (RECOMMENDED)

  • Increasing the security level increases the ability of the system to reject spoof attempts (true positive rate, or TPR). A higher security level also increases the genuine reject rate (false positive rate, or FPR) and the time required by the anti-spoofing module to make a decision.

    For most production scenarios, Keyless recommends the use of PASSIVE_STANDALONE_HIGH. This setting offers a good tradeoff between TPR, FPR, and time-to-decision. For scenarios that require a higher security level, we recommend increasing this setting to PASSIVE_STANDALONE_HIGHEST.

    Configuring livenessEnvironmentAware

    The livenessEnvironmentAware feature enforces stricter environmental checks during the liveness process and can therefore add an additional layer of protection against certain biometric attacks. This is set to false by default from SDK v5.3.3 and above.

    Note that we have observed that, on a very limited set of devices, this may prevent some users from authenticating with Keyless altogether and in this case the SDK will return an 20021 error.

    For security purposes, we have not described how this liveness feature works in detail. Please contact a member of the Keyless team if you would like to better understand how this feature works and the security/UX trade-offs that we observe with either the true / false setting.

    Relax liveness checks for testing purposes

    Below follows a liveness configuration example for testing pruposes only should facilitate testing the happy path of "passing the liveness checks".

    // ONLY FOR TEST
    
    // Authentication Configuration
    val authConfig = BiomAuthConfig(
            livenessConfiguration = LivenessSettings.LivenessConfiguration.PASSIVE_STANDALONE_MEDIUM,
            livenessEnvironmentAware = false
    )
    
    
    // Enrollment Configuration
    val enrollConfig =
    

    protecting the authentication code that you use for dynamic linking.

  • displaying and signing the information to make the payer aware of details of the transaction.

  • Keyless is not a payment service provider. Keyless won't issue an authentication code tied to the transaction information.

    Strong customer authentication (SCA)

    By adding Keyless to your checkout flow you also benefit from Keyless Passwordless Multi Factor Authentication (MFA).

    SCA requires authentication to use at least two of the following three elements.

    • Something that only the customer knows. For example, a password or PIN.

    • Something that only the customer has. For example, a mobile phone or hardware token.

    • Something that the customer is. For example, a biometric such as a fingerprint or face.

    With Keyless Passwordless MFA you can satisfy the last two points from the list above.

    SCA Compliant Dynamic Linking

    The following sections contain some examples on implementing SCA with Keyless.

    Display transaction information

    Keyless displays a screen containing a list of labels and associated information on your behalf. For this reason, the format of the dynamicLinkingInfo must be a jsonArray containing jsonObjects (key/value pairs). We expect a valid JSON as follows:

    This information is added to the Authentication request that the user needs to approve.

    SCA tied to the authentication code

    Once the user approves the transaction data, Keyless starts the authentication to:

    1. Authenticate the payer with the device factor and the biometric factor using Keyless MFA.

    2. Tie the transaction data to the Keyless MFA

    To tie the transaction data to the Keyless MFA, populate the parameter dynamicLinkingInfo of the authentication configuration AuthConfig. Add the authentication code or any other information you want to display to the user and sign with Keyless MFA. For example, add the "authentication code".

    The transactionData contained in dynamicLinkingInfo must respect the format to display transaction information.

    Keyless can produce a signed JWT containing a claim titled td (transaction data) that contains the payload you passed as dynamicLinnkingInfo.

    Keyless is not storing history of records about the transaction amount, the payee, the payer and the authentication code.

    Verify the transaction

    If the authentication is successful, the AuthenticationSuccess contains the following fields:

    • signedJwt: the signed JWT (specs below).

    The external_user_id claim is included only if the user requesting the JWT has an external user id associated

    Verify the JWT using the public key from Keyless backend.

    Congrats, you just performed a Strong Customer Authentication displaying and signing the transaction information.

    SCA PSD2 dynamic linking
    PhotoEnrollConfig
    .
    // photoBitmap is the bitmap you created from the document photo.
    val configuration = PhotoEnrollConfig(photo = photoBitmap)
    
    Keyless.enroll(
      configuration = configuration,
      onCompletion = { result ->
        when (result) {
          is Keyless.KeylessResult.Success ->
    
    // photoUIImage is the UIImage you created from the document photo.
    let configuration = PhotoEnrollConfig(photo: photoUIImage)
    
    Keyless.enroll(
      configuration: configuration,
      onCompletion: { result in
        switch result {
        
    

    Photo Enrollment configuration

    You can configure the enrollment process with optional parameters in your PhotoEnrollConfig() instance.

    Photo Enrollment success result

    If the Enroll from photo is successful you will find in the EnrollmentSuccess containing the corresponding fields you requested during configuration.

    If the enrollment is successful the user is enrolled and can authenticate with Keyless from now on.

    enrollment section
    Keyless Mobile Document SDK

    5️⃣ User and device management

    The Keyless SDK "caches" the enrolled user locally on the device.

    There are some use cases where it is possible to delete the user from server API and delete the device from server API. The Keyless SDK will not be notified about such deletions. For this reason if you try to authenticate a user or a device that have been deleted from server API you will get an error.

    Call validateUserAndDeviceActive before authenticating, to validate that both the user and the device are still active in the Keyless backend, to avoid asking the user for biometric data which will still not let them authenticate.

    Keyless.validateUserAndDeviceActive(
        onCompletion = { result ->
            when (result) {
                is Keyless.KeylessResult.Success -> Log.d("KeylessSDK ", "user and device active")
                is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", 
    
    Keyless.validateUserDeviceActive(
        completionHandler: { error in
            if let error = error {
                print("user or device deactivated")
                // error code 1131 = user is not enrolled on the device (not even locally so did not check on backend)
                // error code 534 = user not found or deactivated on backend
                // error code 535 = device not found or deactivated on backend
    

    User identifier

    Retrieve the user identifier with Keyless.getUserId():

    Device identifier

    The device is identified by its public signing key. To retrieve the public signing key use Keyless.getDevicePublicSigningKey():

    Keyless SDK reset

    Resetting the Keyless SDK to a clean state deletes local data from the device, but does not de-enroll the user from the Keyless backend or deactivate the device from the Keyless backend:

    Lockout management

    The getRateLimitInfo API checks whether the user is currently rate-limited and, if so, for how many seconds. This API is typically used to provide feedback to users after multiple failed authentication attempts.

    Logging

    This page explains how customers can leverage the option to log additional data from the mobile SDK for the purpose of performance monitoring, issue investigation and analytics

    Logging is disabled by default.

    Please note logs do not include any Personally Identifiable Information (PII).

    How it works

    Firstly, there are two options available related to logging data:

    1. If you want to enable logging to the Keyless infrastructure use keylessLogsConfiguration . This options ensures Keyless has access to richer SDK logs to support investigations on our side, and also enriches the .

    2. Alternatively, if you wish to collect logging for you own analytics without sending them to the Keyless infrastructure use customLogsConfiguration

    Collect logs after setting up custom logging with customLogsConfiguration option:

    1. Start collecting the Keyless.customLogs flow (This must happen before a Keyless.configure() to get all Logs Events):

    1. Configure the SDK with the following SetupConfig:

    Logging Levels

    You may set different logging levels to provide more or less information in logs.

    The available levels are the following:

    • INFO (default)

    • DEBUG

    • TRACE

    The TRACE level provides the following additional data:

    • userId

    • devicePublicSigningKey

    • coreLogHistory (used for detailed debugging)

    
    // clientState retrieved from previous step
    let clientState = "<your_client_state>"
    
    let enrollConfig = BiomEnrollConfig(clientState: clientState)
    
    Keyless.enroll(
      configuration: enrollConfig,
      onCompletion: { result in
        switch result {
        case .success(let enrollSuccess):
        	
        	// account recovered
          	let userId = enrollSuccess.userId
    
        case .failure(let error):
            print("error code: \(error.code)
        }
      })
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/enrollment_configuration.dart';
    
    // temporaryState retrieved from previous step
    final temporaryState = "<your_temporary_state>";
    
    final enrollConfig = BiomEnrollConfig(temporaryState: temporaryState);
    
    try {
        final result = await Keyless.instance.enroll(enrollConfig);
        // account recovered
        print("Account recovered successfully. UserID: ${result.keylessId}");
    } catch (error) {
        print("Account recovery failed: $error");
    }
    const clientState = '<your client state';
    const config = new BiomEnrollConfig({
      clientState: clientState,
    });
    
    const result = await Keyless.enroll(config);
    result.fold({
      onSuccess(data) {
        logConsole('Enroll result success ' + JSON.stringify(data, null, 2));
      },
      onFailure(error) {
        logConsole('Enroll result failure ' + JSON.stringify(error, null, 2));
      },
    });
    let enrollConfig = BiomEnrollConfig(generatingClientState: .backup)
    
    Keyless.enroll(
      configuration: enrollConfig,
      onCompletion: { result in
        switch result {
        case .success(let enrollSuccess):
        	
        	let clientState = enrollSuccess.clientState
        	// store the client state on your backend to recover the account in the future
    
        case .failure(let error):
            print("error code: \(error.code)
        }
      })
    let authConfig = BiomAuthConfig(generatingClientState: .backup)
    
    Keyless.authenticate(
      configuration: authConfig,
      onCompletion: { result in
        switch result {
        case .success(let authSuccess):
        	
        	let clientState = authSuccess.clientState
        	// store the client state on your backend to recover the account in the future
    
        case .failure(let error):
            print("error code: \(error.code)
        }
      })
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/enrollment_configuration.dart';
    
    final enrollConfig = BiomEnrollConfig(shouldRetrieveTemporaryState: true);
    
    try {
        final result = await Keyless.instance.enroll(enrollConfig);
        if (result.temporaryState != null) {
            // store the temporary state on your backend to recover the account in the future
            print("Temporary state retrieved: ${result.temporaryState}");
        }
    } catch (error) {
        print("Enrollment failed: $error");
    }
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/authentication_configuration.dart';
    
    final authConfig = BiomAuthConfig(shouldRetrieveTemporaryState: true);
    
    try {
        final result = await Keyless.instance.authenticate(authConfig);
        if (result.temporaryState != null) {
            // store the temporary state on your backend to recover the account in the future
            print("Temporary state retrieved: ${result.temporaryState}");
        }
    } catch (error) {
        print("Authentication failed: $error");
    }
    const config = new BiomEnrollConfig({
      generatingClientState: ClientStateType.BACKUP,
    });
    
    const result = await Keyless.enroll(config);
    result.fold({
      onSuccess(data) {
        logConsole('Enroll result success ' + JSON.stringify(data, null, 2));
        const clientState = data.clientState;
        // store the client state on your backend to recover the account in the future
        logConsole('Client state ' + JSON.stringify(clientState, null, 2));
      },
      onFailure(error) {
        logConsole('Enroll result failure ' + JSON.stringify(error, null, 2));
      },
    });
    const config = new BiomAuthConfig({
      generatingClientState: ClientStateType.BACKUP,
    });
    
    const result = await Keyless.authenticate(config);
    result.fold({
      onSuccess(data) {
        logConsole('Authenticate result success ' + JSON.stringify(data, null, 2));
        const clientState = data.clientState;
        // store the client state on your backend to recover the account in the future
        logConsole('Client state ' + JSON.stringify(clientState, null, 2));
      },
      onFailure(error) {
        logConsole('Authenticate result failure ' + JSON.stringify(error, null, 2));
      },
    });
    "{\"artifact\":{\"family\":\"davideface_lite\",\"version\":\"1.2.0\",\"target\":\"mobile_sdk\",\"liveness\":\"liveness\"},\"core-client-state\":\"BASE_64_STATE\"}"
    // ONLY FOR TEST
    
    // Authentication Configuration
    let authConfig = BiomAuthConfig(
            livenessConfiguration: Keyless.LivenessConfiguration.passiveStandaloneMedium
            livenessEnvironmentAware: false
    )
    
    // Enrollment Configuration
    let enrollConfig = BiomEnrollConfig(
            livenessConfiguration: Keyless.LivenessConfiguration.passiveStandaloneMedium
            livenessEnvironmentAware: false
    )
    
    // De-Enrollment Configuration
    let deEnrollConfig = BiomDeEnrollConfig(
            livenessConfiguration: Keyless.LivenessConfiguration.passiveStandaloneMedium
            livenessEnvironmentAware: false
    )
    // ONLY FOR TEST
    
    
    // Authentication Configuration
    final authConfig = BiomAuthConfig(
        livenessConfiguration: LivenessConfiguration.PASSIVE_STANDALONE_MEDIUM
        // livenessEnvironmentAware: false - not available yet
    );
    
    // Enrollment Configuration
    final enrollConfig = BiomEnrollConfig(
        livenessConfiguration: LivenessConfiguration.PASSIVE_STANDALONE_MEDIUM
        // livenessEnvironmentAware: false - not available yet
    
    );
    
    // De-Enrollment Configuration
    final deEnrollConfig = BiomDeEnrollConfig(
        livenessConfiguration: LivenessConfiguration.PASSIVE_STANDALONE_MEDIUM
        // livenessEnvironmentAware: false - not available yet
    );
    // ONLY FOR TEST
    
    
    // Authentication Configuration
    const authConfig = new BiomAuthConfig({
        livenessConfiguration: LivenessConfiguration.PASSIVE_STANDALONE_MEDIUM,
        livenessEnvironmentAware: false,
    });
    
    // Enrollment Configuration
    const enrollConfig = new BiomEnrollConfig({
        livenessConfiguration: LivenessConfiguration.PASSIVE_STANDALONE_MEDIUM,
        livenessEnvironmentAware: false,
    });
    
    // De-Enrollment Configuration
    const deEnrollConfig = new BiomDeEnrollConfig({
        livenessConfiguration: LivenessConfiguration.PASSIVE_STANDALONE_MEDIUM,
        livenessEnvironmentAware: false,
    });
        
    //Keyless adds a td claim to the JWTs containing the data you specify
    val dynamicLinkingInfo = DynamicLinkingInfo(transactionData = "<your transaction data to display and sign>")
    
    // authenticate with biometric
    val biomAuthConfig = BiomAuthConfig(dynamicLinkingInfo = dynamicLinkingInfo)
    
    // perform the authentication
    Keyless.authenticate(
        configuration = biomAuthConfig,
        onCompletion = { /*TODO: process result*/ }
    )
    //Keyless adds a td claim to the JWTs containing the data you specify
    let dynamicLinkingInfo = DynamicLinkingInfo(transactionData = "<your transaction data to display and sign>")
    
    // authenticate with biometric
    let biomAuthConfig = BiomAuthConfig(dynamicLinkingInfo: dynamicLinkingInfo)
    
    // perform the authentication
    Keyless.authenticate(
        configuration: biomAuthConfig,
        onCompletion: { /*TODO: process result*/ }
    )
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/authentication_configuration.dart';
    import 'package:keyless_flutter_sdk/models/dynamic_linking_info.dart';
    
    final dynamicLinkingInfo = DynamicLinkingInfo(transactionData: "<your transaction data to display and sign>");
    
    // authenticate with biometric
    final biomAuthConfig = BiomAuthConfig(dynamicLinkingInfo: dynamicLinkingInfo);
    
    try {
        // perform the authentication
        final result = await Keyless.instance.authenticate(biomAuthConfig);
        // process the signed JWT from result.signedJwt
        print("Authentication successful. Signed JWT: ${result.signedJwt}");
    } catch (error) {
        print("Authentication failed: $error");
    }
    [
        {key1 : value1},
        {key2 : value2},
        ...
        {keyN: valueN}
    ]
    // JWT header
    {
      "alg": "ES256",
      "typ": "JWT",
      "kid": "PIN/FACE"
    }
    // JWT payload
    {
      "iat": 1720519812,
      "td": "your transaction data to display and sign",
      "version": "1.1.0",
      "sub": "keyless_id",
      "external_user_id": "external user id only if present"
    }
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/enrollment_configuration.dart';
    
    // documentImage from eDocument.
    final configuration = PhotoEnrollConfig(documentImage: documentImage);
    
    try {
      final result = await Keyless.instance.enroll(configuration);
      print("Enrollment finished successfully. UserID: ${result.keylessId}");
    } catch (error) {
      print("Enrollment finished with error: $error");
    }
    const config = new PhotoEnrollConfig({
      photoBase64: '<photoBase64>',
    });
    
    const result = await Keyless.enroll(config);
    result.fold({
      onSuccess(data) {
        logConsole('Enroll result success ' + JSON.stringify(data, null, 2));
      },
      onFailure(error) {
        logConsole('Enroll result failure ' + JSON.stringify(error, null, 2));
      },
    });
    public data class PhotoEnrollConfig(
        public val photo: Bitmap,
        public val temporaryState: String? = null,
        public val operationInfo: OperationInfo? = null,
        public val jwtSigningInfo: JwtSigningInfo? = null
    )
    public struct PhotoEnrollConfig{
        public let photo: CGImage
        public let operationInfo: Keyless.OperationInfo?
        public let jwtSigningInfo: JwtSigningInfo?
        public let temporaryState: String?
    }
    class PhotoEnrollConfig{
      final Uint8List? documentImage;
      final String? temporaryState;
      final OperationInfo? operationInfo;
      final JwtSigningInfo? jwtSigningInfo;
    }
    
     class PhotoEnrollConfig {
      public readonly photoBase64: string;
      public readonly operationInfo: OperationInfo | null;
      public readonly jwtSigningInfo: JwtSigningInfo | null;
      public readonly clientState: string | null;
    
    
    data class EnrollmentSuccess(
        val signedJwt: String? = null,
    ) : KeylessSdkSuccess()
    public struct EnrollmentSuccess {
        public let signedJwt: String?
    }
    class EnrollmentSuccess {
        final String? signedJwt;
    }
     class EnrollmentSuccess {
        public readonly keylessId: string;
        public readonly customSecret: string;
        public readonly enrollmentFrame: string | null;
        public readonly signedJwt: string | null;
        public readonly clientState: string | null;
     }
    try {
      await Keyless.validateUserAndDeviceActive();
      print("user and device active");
    } catch (error) {
      print("user or device deactivated - error code: ${error.code}");
      // error code 1131 = user is not enrolled on the device (not even locally so did not check on backend)
      // error code 534 = user not found or deactivated on backend
      // error code 535 = device not found or deactivated on backend
    }
    import Keyless from '@react-native-keyless/sdk';
    
    const validateUserAndDevice = async () => {
      const result = await Keyless.validateUserAndDeviceActive();
    
      result.fold({
        onSuccess: () => {
          console.log("user and device active");
        },
        onFailure: (error) => {
          console.error(`user or device deactivated - error code: ${error.code}`);
          // error code 1131 = user is not enrolled on the device (not even locally)
          // error code 534 = user not found or deactivated on backend
          // error code 535 = device not found or deactivated on backend
        }
      });
    };
    when (result) {
    is Keyless.KeylessResult.Success -> {
    // account recovered
    val userId = result.value.userId
    }
    is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "error code ${result.error.code}")
    }
    }
    )
    val clientState = result.value.clientState
    // store the client state on your backend to recover the account in the future
    }
    is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "error code ${result.error.code}")
    }
    }
    )
    val clientState = result.value.clientState
    // store the client state on your backend to recover the account in the future
    }
    is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "error code ${result.error.code}")
    }
    }
    )
    BiomEnrollConfig
    (
    livenessConfiguration = LivenessSettings.LivenessConfiguration.PASSIVE_STANDALONE_MEDIUM,
    livenessEnvironmentAware = false
    )
    // De-Enrollment Configuration
    val deEnrollConfig = BiomDeEnrollConfig(
    livenessConfiguration = LivenessSettings.LivenessConfiguration.PASSIVE_STANDALONE_MEDIUM,
    livenessEnvironmentAware = false
    )
    Log.
    d
    (
    "KeylessSDK "
    ,
    "Enroll success - userId
    ${
    result.
    value
    .keylessId
    }
    "
    )
    is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "Enroll failure - error code ${result.error.code}")
    }
    }
    )
    case
    .success
    (
    let
    enrollmentSuccess
    )
    :
    print("Enrollment finished successfully. UserID: \(enrollmentSuccess.keylessId)")
    case .failure(let error):
    print("Enrollment finished with error: \(error.message)
    }
    })
    "user or device not ofund - error code
    ${
    result.error.code
    }
    "
    )
    // error code 1131 = user is not enrolled on the device (not even locally so did not check on backend)
    // error code 534 = user not found or deactivated on backend
    // error code 535 = device not found or deactivated on backend
    }
    }
    )
    } else {
    print("user and device active")
    }
    }
    )
    fun getUserId(): KeylessResult<String, KeylessSdkError>
    func getUserId() -> Result<String, KeylessSDKError>
    Future<String> getUserId() async
    import Keyless from '@react-native-keyless/sdk';
    
    const getUserId = async () => {
      const result = await Keyless.getUserId();
    
      result.fold({
        onSuccess: (userId) => {
          console.log("User ID:", userId);
        },
        onFailure: (error) => {
          console.error("Failed to get user ID:", error);
        },
      });
    };

    Start collecting Keyless suctom logs flow (This must happen before a Keyless.configure() to get all Logs Events)

    1. When you want to unsubscribe from logs make sure you call:

    Keyless dashboard
    val setup = SetupConfig(
        apiKey = "...",
        hosts = listOf(""),
        keylessLogsConfiguration = LogsConfiguration(
            enabled = true,
            logLevel = LogLevels.INFO
        ),
        customLogsConfiguration = LogsConfiguration(
            enabled = true
        )
    )
    let configuration = SetupConfig(
        apiKey: "some api key",
        hosts: ["some.host"],
        keylessLogsConfiguration: KeylessLogsConfiguration(enabled: true),
        customLogsConfiguration: CustomLogsConfiguration(enabled: true, logLevel: .INFO, callback: { event in
            print(event)
        })
    )
    
    Keyless.configure(setupConfiguration: configuration) { error in
        // handle error
    }
    Keyless.customLogs.collect { logEvent ->
        // handle the logEvent
    }
    val setup = SetupConfig(
        apiKey = "...",
        hosts = listOf(""),
        customLogsConfiguration = LogsConfiguration(
            enabled = true,
            logLevel = LogLevels.INFO // This is optional and defaults to INFO
        )
    )
    var myEventCollection = [LogEvent]()
        let configuration = Keyless.SetupConfiguration
          .builder
          .withApiKey("some api key")
          .withHosts(["some.host"])
          .withLogging(
            keylessLogsConfiguration: KeylessLogsConfiguration(enabled: true, logLevel: .INFO),
            customLogsConfiguration: CustomLogsConfiguration(enabled: true, callback: { event in
              myEventCollection.append(event)
            })
          )
         .build()
        Keyless.configure(setupConfiguration: configuration) { error in
          // handle error
        }
    }
    fun getDevicePublicSigningKey(): KeylessResult<ByteArray, KeylessSdkError>
    func getDevicePublicSigningKey() -> Result<String, KeylessSDKError>
    Future<String> getDevicePublicSigningKey() async
    import Keyless from '@react-native-keyless/sdk';
    
    const getDevicePublicSigningKey = async () => {
      const result = await Keyless.getDevicePublicSigningKey();
    
      result.fold({
        onSuccess: (key) => {
          console.log("Device public key:", key);
        },
        onFailure: (error) => {
          console.error("Failed to get device public key:", error);
        },
      });
    };
    fun reset(
      onCompletion: (KeylessResult<Unit, KeylessSdkError>) -> Unit
    )
    func reset() -> KeylessSDKError?
    Future<void> reset() async
    import Keyless from '@react-native-keyless/sdk';
    
    const resetSdk = async () => {
      const result = await Keyless.reset();
    
      result.fold({
        onSuccess: () => {
          console.log("SDK reset successfully");
        },
        onFailure: (error) => {
          console.error("SDK reset failed:", error);
        },
      });
    };
    Keyless.getRateLimitInfo { result ->
        when (result) {
            is Keyless.KeylessResult.Success -> {
                val rateLimitInfo = result.value
                println("User is rate limited: ${rateLimitInfo.isRateLimited} with remaining seconds: ${rateLimitInfo.remainingSeconds}")
            }
            is Keyless.KeylessResult.Failure -> {
                println("Error: ${result.error.message}")
            }
        }
    }
    Keyless.getRateLimitInfo(completion: { result in
        switch result {
        case .success(let success):
            print("User is rate limited: \(success.isRateLimited) with remaining seconds: \(success.remainingSeconds)")
        case .failure(let error):
            print("Error: \(error.message)")
        }
    })
    try {
        final rateLimitInfo = await Keyless.instance.getRateLimitInfo();
        print("User is rate limited: ${rateLimitInfo.isRateLimited} with remaining seconds: ${rateLimitInfo.remainingSeconds}");
    } catch (error) {
        print("Error: $error");
    }
    import Keyless from '@react-native-keyless/sdk';
    
    const getRateLimitInfo = async () => {
      const result = await Keyless.getRateLimitInfo();
    
      result.fold({
        onSuccess: (info) => {
          console.log(`User is rate limited: ${info.isRateLimited}, remaining seconds: ${info.remainingSeconds}`);
        },
        onFailure: (error) => {
          console.error("Error fetching rate limit info:", error);
        },
      });
    };
    val setupConfiguration = SetupConfiguration.builder
        .withApiKey("")
        .withHosts(listOf("..."))
        .withLogging(
            keylessLogsConfiguration = LogsConfiguration(enabled = true),
            customLogsConfiguration = LogsConfiguration(enabled = true, logLevel = LogLevels.INFO)
        )
        .build()
    let configuration = Keyless.SetupConfiguration
      .builder
      .withApiKey("some api key")
      .withHosts(["some.host"])
      .withLogging(
        keylessLogsConfiguration: KeylessLogsConfiguration(enabled: true, logLevel: .INFO),
        customLogsConfiguration: CustomLogsConfiguration(enabled: true, callback: { event in
          print(event)
        })
      )
      .build()
     
    Keyless.configure(setupConfiguration: configuration) { error in
      // handle error
    }
    final configuration = SetupConfiguration(
                  apiKey: apiKey,
                  hosts: [host],
                  loggingEnabled: true,
                  loggingLevel: LogLevel.info));
    
    try {
      final result = await Keyless.instance.configure(configuration);
      print("Configure finished succcessfully.");
    } catch (error) {
      print("Configure finished with error: $error");
    }
        const config = new SetupConfig({
          apiKey: 'apiKey',
          hosts: ['HOSTS'],
          keylessLogsConfiguration: new LogsConfiguration({
            enabled: true,
            logLevel: LogLevels.INFO,
          }),
          customLogsConfiguration: new LogsConfiguration({
            enabled: true,
            logLevel: LogLevels.TRACE,
          }),
        });
        const result = await Keyless.configure(config);
    
        result.fold({
          onSuccess: _data => {
            //Handle success
          },
          onFailure: _error => {
            //Handle error
          },
    
        const eventSubscription = Keyless.subscribeToCustomLogs(eventLog => {
            // Handle log
        });
        const config = new SetupConfig({
          apiKey: 'apiKey',
          hosts: ['HOSTS'],
          customLogsConfiguration: new LogsConfiguration({
            enabled: true,
            logLevel: LogLevels.TRACE,
          }),
        });
        const result = await Keyless.configure(config);
    
        result.fold({
          onSuccess: _data => {
            //Handle success
          },
          onFailure: _error => {
            //Handle error
          },
    
        eventSubscription?.unsubscribe();

    Get all user authentications in the last 10 minutes

    get

    Get all user authentications in the last 10 minutes. This can be optionally filtered by operation id in order to return a specific transaction/authentication attempt.

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    userIdstring · Uppercase HEX stringRequired

    The user id

    Query parameters
    operationIdstringOptional

    Operation id to filter the results

    Responses
    200

    User authentications

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    User not found

    application/json
    get
    /users/{userId}/authentications

    Get all user authentications in the last 10 minutes using the External User ID

    get

    Get all user authentications in the last 10 minutes using the External User ID (i.e. the unique identifier the user is identified by in your own system). This can be optionally filtered by operation id in order to return a specific transaction/authentication attempt.

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    externalUserIdstringRequired

    The customer-meaningful user id. Case-sensitive

    Query parameters
    operationIdstringOptional

    Operation id to filter the results

    Responses
    200

    External user authentications

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    External user not found

    application/json
    get
    /external-users/{externalUserId}/authentications

    Get user devices

    get

    Get user devices

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    userIdstring · Uppercase HEX stringRequired

    The user id

    Responses
    200

    User devices

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    get
    /users/{userId}/devices

    Delete user device

    delete

    Delete user device

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    userIdstring · Uppercase HEX stringRequired

    The user id

    publicSigningKeystringRequired

    Public signing key

    Responses
    200

    Device successfully deleted

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    delete
    /users/{userId}/devices/{publicSigningKey}

    Get a user's pending operations

    get

    Get a user's pending operations

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    userIdstring · Uppercase HEX stringRequired

    The user id

    Responses
    200

    An user's pending operations

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    get
    /users/{userId}/operations/pending

    Get a customer operation

    get

    Get a customer operation

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    operationIdstringRequired

    Unique operation identifier set and managed by the client

    Responses
    200

    A customer operation

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    get
    /operations/{operationId}

    Create a pending operation

    post

    Create a pending operation

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    userIdstring · Uppercase HEX stringRequired

    The user id

    Body
    externalUserIdstringOptional

    User id set and managed by the client

    operationIdstringRequired

    Unique operation identifier set and managed by the client

    operationPayloadstringOptional

    Operation payload set and managed by the client

    Responses
    200

    Operation created successfully

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    409

    The requested operation conflicts with the current state of the server

    application/json
    415

    Media type not supported, the request can't be processed

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    post
    /users/{userId}/operations

    Get the customer public key

    get

    Get the customer public key in PEM format

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Responses
    200

    The public key in PEM format

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    406

    Not acceptable representation

    application/json
    get
    /verify-jwt/public-key

    Verify a signed JWT message

    post

    Verify a signed JWT message. The only format accepted for now is a JWT generated by a PIN signature

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Body
    messagestring · JWTRequired

    The signed JWT message

    Responses
    200

    The verification completed successfully. Check the result to see if the signature is valid or not

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    406

    Not acceptable representation

    application/json
    415

    Media type not supported, the request can't be processed

    application/json
    post
    /verify-jwt

    Delete an user

    delete

    Delete an user and all associated entities

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    userIdstring · Uppercase HEX stringRequired

    The user id

    Responses
    200

    User successfully deleted

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    delete
    /users/{userId}

    Create an external user

    post

    Create an external user. A user can only have one external user associated

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    userIdstring · Uppercase HEX stringRequired

    The user id

    Body
    externalUserIdstringOptional

    User id set and managed by the client

    Responses
    201

    External user created

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    409

    The requested operation conflicts with the current state of the server

    application/json
    415

    Media type not supported, the request can't be processed

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    post
    /users/{userId}/external-user

    Modify an external user

    patch

    Modify an external user. Idempotent, can be retried

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    userIdstring · Uppercase HEX stringRequired

    The user id

    Body
    externalUserIdstringOptional

    User id set and managed by the client

    Responses
    200

    External user patched

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    415

    Media type not supported, the request can't be processed

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    patch
    /users/{userId}/external-user

    Delete an external user

    delete

    Delete an external user. The external user id is case-sensitive

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    externalUserIdstringRequired

    The customer-meaningful user id. Case-sensitive

    Responses
    204

    External user deleted. If the external user does not exist, the response is still positive

    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    delete
    /external-users/{externalUserId}

    No content

    Get all users associated to external user id

    get

    Get all users associated to external user id. Returns an empty list if none is found. External user id is case-insensitive

    Authorizations
    X-Api-KeystringRequired

    The customer service api key

    Path parameters
    externalUserIdstringRequired

    The customer-meaningful user id. Case-sensitive

    Responses
    200

    All users found bound to external user id. Empty if none is found

    application/json
    400

    Request is invalid and shouldn't be retried if the request is unchanged

    application/json
    401

    Unauthorized request, the request can't be processed unless you provide a valid authentication method

    application/json
    404

    Resource not found

    application/json
    406

    Not acceptable representation

    application/json
    409

    The requested operation conflicts with the current state of the server

    application/json
    500

    An internal error occurred, please try again later or contact the support.

    application/json
    get
    /external-users/{externalUserId}/users
    {
      "page": {
        "number": 1,
        "totalElements": 1,
        "totalPages": 1,
        "size": 1
      },
      "content": [
        {
          "userId": "0123456789ABCDEF",
          "authentications": [
            {
              "createdAt": "text",
              "result": "APPROVED",
              "errorCode": 1,
              "operationId": "text",
              "deviceId": "text",
              "authType": "biom"
            }
          ]
        }
      ]
    }
    {
      "page": {
        "number": 1,
        "totalElements": 1,
        "totalPages": 1,
        "size": 1
      },
      "content": [
        {
          "externalUserId": "text",
          "authentications": [
            {
              "createdAt": "text",
              "result": "APPROVED",
              "errorCode": 1,
              "operationId": "text",
              "deviceId": "text",
              "authType": "biom"
            }
          ]
        }
      ]
    }
    GET /v2/users/{userId}/authentications HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    
    GET /v2/external-users/{externalUserId}/authentications HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    
    GET /v2/users/{userId}/devices HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    
    [
      {
        "userId": "0123456789ABCDEF",
        "sdkCustomerId": 1,
        "publicSigningKey": "text",
        "publicEncryptionKey": "text",
        "state": "ACTIVE",
        "createdAt": "2020-01-02T03:04:05.242194378",
        "osVersion": "text",
        "sdkVersion": "text",
        "deletedAt": "2020-01-02T03:04:05.242194378"
      }
    ]
    DELETE /v2/users/{userId}/devices/{publicSigningKey} HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    
    {
      "success": true
    }
    {
      "success": true
    }
    {
      "result": true
    }
    GET /v2/users/{userId}/operations/pending HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    
    [
      {
        "operationId": "123456abcdef",
        "operationPayload": "payload",
        "externalUserId": "abcdef123456",
        "authType": "biom"
      },
      {
        "operationId": "654321fedcba"
      }
    ]
    GET /v2/operations/{operationId} HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    
    {
      "id": 1,
      "timestamp": "2020-01-02T03:04:05.68",
      "nodeId": "text",
      "userId": "0123456789ABCDEF",
      "apiKey": "text",
      "externalUserId": "text",
      "operationId": "text",
      "operationPayload": "text",
      "result": true,
      "authType": "biom",
      "state": "approved"
    }
    POST /v2/users/{userId}/operations HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Content-Type: application/json
    Accept: */*
    Content-Length: 72
    
    {
      "externalUserId": "text",
      "operationId": "text",
      "operationPayload": "text"
    }
    GET /v2/verify-jwt/public-key HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    
    {
      "content": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwJ1\n-----END PUBLIC KEY-----"
    }
    POST /v2/verify-jwt HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Content-Type: application/json
    Accept: */*
    Content-Length: 18
    
    {
      "message": "text"
    }
    DELETE /v2/users/{userId} HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    
    {
      "success": true
    }
    {
      "sdkCustomerId": 1,
      "userId": "0123456789ABCDEF",
      "externalUserId": "text",
      "createdAt": "2025-08-06T15:04:05.242194378",
      "updatedAt": "2025-08-06T15:04:05.242194378"
    }
    {
      "sdkCustomerId": 1,
      "userId": "0123456789ABCDEF",
      "externalUserId": "text",
      "createdAt": "2025-08-06T15:04:05.242194378",
      "updatedAt": "2025-08-06T15:04:05.242194378"
    }
    [
      {
        "userId": "0123456789ABCDEF",
        "biometricPublicSigningKey": "0123456789ABCDEF",
        "createdAt": "2025-08-06T15:04:05.242194378",
        "updatedAt": "2025-08-06T15:04:05.242194378"
      }
    ]
    POST /v2/users/{userId}/external-user HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Content-Type: application/json
    Accept: */*
    Content-Length: 25
    
    {
      "externalUserId": "text"
    }
    PATCH /v2/users/{userId}/external-user HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Content-Type: application/json
    Accept: */*
    Content-Length: 25
    
    {
      "externalUserId": "text"
    }
    DELETE /v2/external-users/{externalUserId} HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    
    GET /v2/external-users/{externalUserId}/users HTTP/1.1
    Host: 
    X-Api-Key: YOUR_API_KEY
    Accept: */*
    

    PIN authentication

    Use a PIN to perform specific operations when biometric authentication is not necessary

    For authentication scenarios that don't necessitate biometric recognition, you can use PIN as an alternative.

    PIN authentication has limited capabilities compared to biometric authentication. PIN supports:

    • operation info,

    • ,

    Keyless requires at least one of the following authentication factor to be present for each user:

    • biometric factor

    • PIN factor

    PIN factor can be any valid String. Numbers are not enforced but are recommended, given the familiarity of numeric PINs for end users.

    Enrollment with PIN

    To enroll using the PIN factor create the following configuration:

    To enroll multiple authentication factors you need call Keyless.enroll for each factor.

    Authentication with PIN

    To authenticate using the PIN factor create the following configuration:

    De-enrollment with PIN

    Note that de-enrolling deletes the user biometric factor as well as the PIN factor. If you just want to remove the PIN authentication factor, use the instead.

    To de-enroll using the PIN authentication factor, create the following configuration:

    Remove PIN (keeping biometric factor)

    To remove the PIN factor, while still keeping the biometric factor, perform a biometric authentication using the following configuration:

    PIN utilities

    To change the PIN use the newPin parameter in the PinAuthConfig:

    To remove the PIN factor and keep the user enrolled with the biometric factor use the shouldRemovePin parameter in PinAuthConfig:

    jwt signing
    PIN utilities
    val configuration = PinEnrollConfig(pin = "1234")
    
    Keyless.enroll(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration =  PinEnrollConfig(pin: "1234")
    
    Keyless.enroll(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    val configuration = EnrollmentConfiguration.builder
      .withPin("1234")
      .build()
    
    Keyless.enroll(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration = Keyless.EnrollmentConfiguration.builder
      .withPin("1234")
      .build()
    
    Keyless.enroll(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    final configuration = PinEnrollConfig(
        pin: "myPin"
    );
    
    try {
        await Keyless.instance.enroll(configuration);
        print("Enrollment successful");
    } catch (error) {
        print("Enrollment failed: $error");
    }
    const config = new PinEnrollConfig({
      pin: '1234',
    });
    
    const result = await Keyless.enroll(config);
    result.fold({
      onSuccess(data) {
        logConsole('Auth result success ' + JSON.stringify(data, null, 2));
      },
      onFailure(error) {
        logConsole('Enroll result failure ' + JSON.stringify(error, null, 2));
      },
    });
    val configuration = PinAuthConfig(pin = "1234")
    
    Keyless.authenticate(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration =  PinAuthConfig(pin: "1234")
    
    Keyless.authenticate(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    val configuration = AuthenticationConfiguration.builder
      .withPin("1234")
      .build()
    
    Keyless.authenticate(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration = Keyless.AuthenticationConfiguration.builder
      .withPin("1234")
      .build()
    
    Keyless.authenticate(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    final configuration = PinAuthConfig(
        pin: "1234"
    );
    
    try {
        final result = await Keyless.instance.authenticate(configuration);
        print("Authentication successful");
    } catch (error) {
        print("Authentication failed: $error");
    }
    const config = new PinAuthConfig({
      pin: '1234',
    });
    
    const result = await Keyless.authenticate(config);
    result.fold({
      onSuccess(data) {
        logConsole('Auth result success ' + JSON.stringify(data, null, 2));
      },
      onFailure(error) {
        logConsole('Auth result failure ' + JSON.stringify(error, null, 2));
      },
    });
    val configuration = PinDeEnrollConfig(pin = "1234")
    
    Keyless.deEnroll(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration =  PinDeEnrollConfig(pin: "1234")
    
    Keyless.deEnroll(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    val configuration =
    DeEnrollmentConfiguration.builder
      .withPin("myPin")
      .build()
    
    Keyless.deEnroll(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration =
    Keyless.DeEnrollmentConfiguration.builder
      .withPin("myPin")
      .build()
    
    Keyless.deEnroll(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    final configuration = PinDeEnrollConfig(
        pin: "1234"
    );
    
    try {
        await Keyless.instance.deEnroll(configuration);
        print("De-enrollment successful");
    } catch (error) {
        print("De-enrollment failed: $error");
    }
    const config = new PinDeEnrollConfig('1234');
    
    const result = await Keyless.deEnroll(config);
    result.fold({
      onSuccess(data) {
        logConsole('DeEnroll result success ' + JSON.stringify(data, null, 2));
      },
      onFailure(error) {
        logConsole('DeEnroll result failure ' + JSON.stringify(error, null, 2));
      },
    });
    val configuration = BiomAuthConfig(shouldRemovePin = true)
    
    Keyless.authenticate(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration =  BiomAuthConfig(shouldRemovePin: true)
    
    Keyless.authenticate(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    val configuration =
    AuthenticationConfiguration.builder
      .removingPin()
      .build()
    
    Keyless.authenticate(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration =
    Keyless.AuthenticationConfiguration.builder
      .removingPin()
      .build()
    
    Keyless.authenticate(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    final configuration = BiomAuthConfig(
        shouldRemovePin: true
    );
    
    try {
        await Keyless.instance.authenticate(configuration);
        print("Pin remove successful");
    } catch (error) {
        print("Pin remove failed: $error");
    }
    
    const config = new BiomAuthConfig({
      shouldRemovePin: true,
    });
    
    const result = await Keyless.deEnroll(config);
    result.fold({
      onSuccess(data) {
        logConsole('Auth result success ' + JSON.stringify(data, null, 2));
      },
      onFailure(error) {
        logConsole('Auth result failure ' + JSON.stringify(error, null, 2));
      },
    });
    val configuration = PinAuthConfig(pin = "1234", newPin = "5678")
    
    Keyless.authenticate(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration =  PinAuthConfig(pin: "1234", newPin: "5678")
    
    Keyless.authenticate(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    Future<void> changePin({
      required String oldPin,
      required String newPin
    }) async
    const config = new PinAuthConfig({
      pin: '1234',
      newPin: '5678',
    });
    
    const result = await Keyless.authenticate(config);
    result.fold({
      onSuccess(data) {
        logConsole('Auth result success ' + JSON.stringify(data, null, 2));
      },
      onFailure(error) {
        logConsole('Auth result failure ' + JSON.stringify(error, null, 2));
      },
    });
    val configuration = PinAuthConfig(pin = "1234", shouldRemovePin = true)
    
    Keyless.authenticate(
      configuration = configuration,
      onCompletion = { /*TODO: process result*/  }
    )
    let configuration =  PinAuthConfig(pin: "1234", shouldRemovePin: true)
    
    Keyless.authenticate(
      configuration: configuration,
      onCompletion: { /*TODO: process result*/  }
    )
    Future<void> removePin({
      required String pin
    }) async
    const config = new PinAuthConfig({
      pin: '1234',
      shouldRemovePin: true,
    });
    
    const result = await Keyless.authenticate(config);
    result.fold({
      onSuccess(data) {
        logConsole('Auth result success ' + JSON.stringify(data, null, 2));
      },
      onFailure(error) {
        logConsole('Auth result failure ' + JSON.stringify(error, null, 2));
      },
    });

    Error handling

    Common errors, what they mean and recommended next steps.

    The Keyless SDK uses three classes of errors, each error has an error code and an error message. Errors follow 3 main categories:

    • User errors: triggered by unintended or suspicious user behavior.

      • 30_000 and above.

    • : triggered by a KeylessSDK integration misconfiguration.

      • These span from 20_000 to 30_000.

    • : triggered by Keyless internals.

      • All errors below 20_000

    If you're implementing the Keyless SDK, you should handle errors coming from the SDK since the error message is not intended for end users.

    User errors

    User errors have code that are 30_000 and above.

    Note that many of these errors are predictions only, when writing messages for your users it's often best to assume positive intent. For example, suggest that they make sure that nothing is obstructing the camera instead of suggesting that they are attempting to spoof.

    Error
    Code
    Description

    Integration errors

    Integration errors have codes that span from 20_000 to 30_000.

    Integration errors can be solved by making sure you are not misusing the API surface of the SDK. You can solve it by reading the error message and addressing the issue. If errors persist, please keep the error code, error message and stacktrace and contact us.

    Error
    Code
    Description

    Internal errors

    All errors below 20_000 are classed as internal errors in that they relate to the response from our Core platform. Typically internal errors require investigation from Keyless support, however we've highlighted some of the more common ones here where action may be taken by the integrator.

    Error
    Code
    Description
    Notes / Recommendation

    If errors in this range persist and are unclear, please keep the error code, error message and stacktrace and contact us. If possible we'd also recommend enabling at TRACE level and sending these logs to us to further speed up the Keyless investigation.

    Anti-Inject errors

    The errors in the following table apply to Keyless Anti-Inject variant only.

    Error
    Code
    Description

    Examples

    2️⃣ Enrollment

    Enrollment is the process of registering a new user by connecting their facial biometrics to a Keyless account. During this process, a full and unobstructed view of the user's face is required.

    enrollUser();

    3️⃣ Authentication

    Authentication is the biometric equivalent of "signing-in". During authentication Keyless compares the user's facial biometrics with the ones computed during .

    If the biometrics match, Keyless authenticates the user.

    Face not matching

    30004

    The face of user in front of the camera does not match the face currently enrolled with Keyless.

    No network connection

    30005

    The device appears to be offline.

    Device tampered

    30006

    The device seems tampered and could have been rooted or jailbroken.

    User lockout

    30007

    The user is temporarily locked out of Keyless after too many failed authentication attempts. Note in case of a a a different lockout code 523 is returned.

    Rejected

    30008

    Keyless did not manage to recognize the user but does not suspect any spoofing attempt.

    Camera denied

    30009

    The user denied camera permission.

    NET_CONNECTION_FAILED

    1004

    Network unavailable or server response unexpected.

    If networking is available refer to 507.

    CLIENT_INVALID_INPUT

    1134

    Incorrect or invalid parameter passed to a core function.

    This may be solved by re-running the Keyless enrollment or authentication request. Alternatively the SDK and core client sync may be in a corrupted state.

    Spoofing

    30000

    The user might be placing a picture or a video in front of the camera.

    Timeout

    30001

    The face could not be recognized before the specified timed out. Note: This error is no longer returned from SDK version 5.0.1 and above given the Liveness timeout feature was deprecated. See Changelog for details.

    Mask detected

    30002

    The user might be wearing a mask, or there might be something hiding their face. Note: mask detected will be part of live feedback and no longer returned as an error from SDK version 4.8.0 and above.

    User cancelled

    30003

    The user manually cancelled the face recognition and processing.

    Liveness Environment Aware

    20021

    The device does not meet the requirements for environment-aware liveness detection.

    PROTOCOL_INVALID_MESSAGE

    507

    client-server mismatch suggesting an issue in communicating with the Core backend.

    This could be due the network dropping, or an outdated (unsupported) core client trying to contact the latest core backend.

    PROTOCOL_FAILED_TO_AUTHENTICATE_USER

    512

    The given user selfie was did not meet the threshold of an approved match - in simple terms we were not confident enough that this was the same face as the one registered to this Keyless ID

    In most cases we advise our customers to advise positive intent and request that the user retries reminding them to make sure they are in a well lit environment, their face is centred and they do not anything covering their face.

    Of course integrators should consider that this error code is triggered because the person attempting to authenticate is not the same person that is registered and handle accordingly.

    PROTOCOL_MAX_NUMBER_OF_DEVICES_REACHED

    539

    The maximum number of devices in this has been reached. Note a maximum of 50 devices per Keyless ID is allowed. Note also that these do not all necessarily represent physical devices of a user (they could be backup or temporary client states that can be use to bind future devices for example).

    Anti-inject initialization failed

    40000

    Keyless Anti-Inject variant only. The initialization of the Anti-Inject failed for an internal error.

    License expired

    40001

    Keyless Anti-Inject variant only. The license "configFile.tak" is expired, contact Keyless team to receive an updated license.

    Device might not be genuine.

    40002

    Keyless Anti-Inject variant only. We have detected signals that this device may be compromised or may not be genuine. This error prevents a user from continuing and it won't be possible to enroll or authenticate.

    Integration errors
    Internal errors
    Keyless logging

    Revoke some of the existing states/devices in order to generate new ones.

    For SaaS customers this can be done either manually via the or programmatically via .

    Enrollment configuration

    You can configure the enrollment process with optional parameters in your BiomEnrollConfig() instance or using the builder pattern methods from the EnrollmentConfiguration builder.

    public data class BiomEnrollConfig(
        public val cameraDelaySeconds: Int = 2,
        public val customSecret: String?,
        public val jwtSigningInfo: JwtSigningInfo?,
    
    
    public struct BiomEnrollConfig {
        public let cameraDelaySeconds: Int
        public let customSecret: String?
        public let jwtSigningInfo: JwtSigningInfo?
        public let livenessConfiguration: Keyless.LivenessConfiguration
    
    

    Enrollment success result

    Depending on the builder methods you enable, Keyless will populate the corresponding fields in the EnrollmentSuccess result reported below.

    Delaying the Keyless evaluation/decision

    By default, our biometric decision making process initiates immediately on the camera being invoked and the camera preview showing. We believe this offers the maximum level of security in that an attacker is given no additional preparation time in front of the camera view, while at the same time we have looked to optimise the experience for genuine users i.e. delivering both approve and reject decisions in a way that feels natural and understandable to users. However, we recognise that our customers, and their users, have different contexts and preferences and therefore the cameraDelaySeconds configuration is available to specify the delay (in seconds) between when the camera preview appears, and when the liveness processing starts. In effect, no decision, whether \

    Please note we advise careful consideration when implementing this feature for two reasons: i) While this allows users to frame themselves and have longer to understand what is happening, is also time for any attackers to also optimise their framing. ii) Implementing will ultimately mean that the "happy path" flow for all users is extended. If the delay is set for too long, some customers have noted that there is also the potential for some users to become frustrated and cancel/drop the flow. We're happy to engage further in what the best trade-offs may be for customers, given our wide-ranging experience of assisting customers in live implementations.

    Custom secret

    During enrollment you can specify a custom secret to be saved and encrypted along with the user's biometric data using savingSecret paramter. The custom secret can be anything you can save as an ASCII string, such as a secret that you have provided to the app from the backend, the seed of an OTP protocol, or anything else.

    JWT Signing info

    You can specify a payload to be added to a JWT signed by Keyless with the jwtSigningInfo parameter, more in JWT signing.

    Liveness Settings

    Using livenessConfiguration you can configure the liveness security level during enrollment. The possible liveness configuration are under LivenessSettings.LivenessConfiguration :

    You can also specify a livenessEnvironmentAware that is by default se to true to enhance liveness detection. This parameters helps to ensure the user is in a suitable setting for verification.

    More details on liveness in the dedicated liveness settings section.

    Operation info

    The parameter operationInfo specifies a customizable unique operation identifier and associated payload stored on the Keyless backend if the enrollment succeeds. Use this to add an extra level of confirmation in your operations.

    Details on how to query our backend for stored operations are available on Operations API.

    Client State

    Keyless users can be enrolled via IDV-Bridge, Identity Verification Bridge. As a result of IDV-Bridge enrollment you receive a client state useful to register users in your app without undergoing the full enrollment flow.

    Use the clientState parameter to register users from a client state obtained through IDV-Bridge.

    You can also use a client state to recover an account of an existing user who lost access to the account. Follow the guide on account recovery.

    Enrollment Frame

    Integrators can specify whether or not to retrieve an enrollment frame, acquired during the user’s selfie capture during the enrollment or account recovery flow. You can achieve this by setting shouldReturnEnrollmentFrame to true on the BiomEnrollmentConfig (defaults to false). If the enrollment succeeds, you can retrieve the frame from the returned EnrollmentSuccess, via enrollmentFrame.

    Enrollment circuits

    To speed up enrollment you can use the numberOfEnrollmentCircuits to upload less than the default of five circuits upon enrollment. The remainder (50 - numberOfEnrollmentCircuits) are uploaded asynchronously.

    val configuration = BiomEnrollConfig()
    
    Keyless.enroll(
      configuration = configuration,
      onCompletion = { result ->
        when (result) {
          is Keyless.KeylessResult.Success -> Log.d("KeylessSDK ", "Enroll success - userId ${result.value.keylessId}")
          is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "Enroll failure - error code ${result.error.code}")
        }
      }
    )
    let configuration = BiomEnrollConfig()
    
    Keyless.enroll(
      configuration: configuration,
      onCompletion: { result in
        switch result {
        case .success(let enrollmentSuccess):
            print("Enrollment finished successfully. UserID: \(enrollmentSuccess.keylessId)")
        case .failure(let error):
            print("Enrollment finished with error: \(error.message)
        }
      })
    Authentication configuration

    You can configure the authentication process with optional parameters in your BiomAuthConfig() instance or using the builder pattern methods from the AuthenticationConfiguration builder.

    public data class BiomAuthConfig(
        public val cameraDelaySeconds: Int = 0,
        public val jwtSigningInfo: JwtSigningInfo?,
        public val livenessConfiguration: LivenessSettings.LivenessConfiguration =
    
    public struct BiomAuthConfig: AuthConfig {
        public let cameraDelaySeconds: Int
        public let jwtSigningInfo: JwtSigningInfo?
        public let livenessConfiguration: Keyless.LivenessConfiguration
        public let livenessEnvironmentAware: 
    

    The successAnimationEnabled and later showScreenSuccessFlow field has been renamed to showSuccessFeedback, triggering a breaking change. Moreover the success animation is now shown by default.

    Authentication success result

    Depending on the builder methods you enable, Keyless will populate the corresponding fields in the AuthenticationSuccess result reported below.

    Backup data

    Backup data is no longer recommended to perform account recovery and the feature has been removed from Android and iOS SDKs. Use the client state instead. Follow the guide on account recovery.

    Keyless can generate backup data that you can use to recover an account.

    To create the backup data use the shouldRetrieveBackup configuration parameter. Once authentication succeeds, copy the backup data from the AuthenticationSuccess result, and store it securely.

    To recover an account, use backup parameter during enrollment more in backup.

    Delaying the Keyless evaluation/decision

    By default, our biometric decision is set at a default two second delay between the camera preview appearing and the liveness evaluation beginning. We believe this offers an acceptable balance between usability and security i.e. delivering both approve and reject decisions in a way that feels natural and understandable to users. However, we recognise that our customers, and their users, have different contexts and preferences and therefore the cameraDelaySeconds configuration is available to change the delay (in seconds) between when the camera preview appears, and when the liveness evaluation starts. In effect, no decision, whether \

    Please note we advise careful consideration when implementing this feature for two reasons: i) While this allows users to frame themselves and have longer to understand what is happening, is also time for any attackers to also optimise their framing. ii) Implementing will ultimately mean that the "happy path" flow for all users is extended. If the delay is set for too long, some customers have noted that there is also the potential for some users to become frustrated and cancel/drop the flow. We're happy to engage further in what the best trade-offs may be for customers, given our wide-ranging experience of assisting customers in live implementations.

    Custom Secret

    If you saved a custom secret during enrollment, you can retrieve it using the shouldRetrieveSecret parameter.

    Keyless will populate the field customSecret in the AuthenticationSuccess result.

    Furthermore, such a custom secret can be deleted using the shouldDeleteSecret parameter.

    JWT Signing info

    You can specify a payload to be added to a JWT signed by Keyless with the jwtSigningInfo parameter, more in JWT signing.

    Liveness Settings

    Using livenessConfiguration you can configure the liveness security level during enrollment. The possible liveness configuration are under LivenessSettings.LivenessConfiguration :

    You can also specify a livenessEnvironmentAware that is by default se to true to enhance liveness detection. This parameters helps to ensure the user is in a suitable setting for verification.

    More details on liveness in the dedicated liveness settings section.

    Operation info

    The parameter operationInfo specifies a customizable unique operation identifier and associated payload stored on the Keyless backend if the authentication succeeds.

    Details on how to query our backend for stored operations are available on Operations API.

    Client State

    Use the generatingClientState parameter of the BiomEnrollConfig or BiomAuthConfig to creata a client state useful for the account recovery.

    Camera Preview Customization (BETA)

    Use the presentationStyle parameter in BiomAuthConfig to control camera preview behavior during authentication.

    • .cameraPreview (default): Shows the standard live camera preview for user guidance.

    • .noCameraPreview: Hides the camera preview entirely, enabling a faster, minimal interface similar to native biometric flows on mobile devices.

    ℹ️ UI Customization Note UI customization is not supported when using .noCameraPreview

    enrollment
    val configuration = BiomAuthConfig()
    
    Keyless.authenticate(
        configuration = configuration,
        onCompletion = { result ->
            when (result) {
                is Keyless.KeylessResult.Success -> Log.d("KeylessSDK ", "Authentication success")
                is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "Authentication failure - error code ${result.error.code}")
            }
        }
    )
    let configuration = BiomAuthConfig()
    
    Keyless.authenticate(
        configuration: configuration,
        onCompletion: { result in
            switch result {
            case .success(let success):
                print("Authentication success")
            case .failure(let error):
                break
            }
        })
    val configuration = Keyless.AuthenticationConfiguration()
    
    Keyless.authenticate(
        authenticationConfiguration = configuration,
        onCompletion = { result ->
            when (result) {
                is Keyless.KeylessResult.Success -> {
                    Log.d("IntegratorActivity ", "Authenticate success")
                }
                is Keyless.KeylessResult.Failure -> {
                    when (result.error) {
                        is KeylessUserError.FaceNotMatching -> Log.d("IntegratorActivity ", "Face not matching")
                        is KeylessUserError.MaskDetected -> Log.d("IntegratorActivity ", "Mask detected")
                        is KeylessUserError.Spoofing -> Log.d("IntegratorActivity ", "Spoofing detected")
                        is KeylessUserError.Timeout -> Log.d("IntegratorActivity ", "The operation timed out")
                        is KeylessUserError.UserCancelled -> Log.d("IntegratorActivity ", "The user cancelled the operation")
                        is KeylessUserError.NoNetworkConnection -> Log.d("IntegratorActivity ", "No network connection available")
                        is KeylessUserError.Lockout -> Log.d("IntegratorActivity ", "Your account is temporarily locked")
                        else -> {
                            Log.d("IntegratorActivity ", "Authenticate failure")
    
                            val errorCode = result.error.code
                            val errorMessage = result.error.message
                            val errorCause = result.error.cause?.printStackTrace()
                            // here you could display a generic error popup with the error code
                        }
                    }
                }
            }
        }
    )
    let configuration = Keyless.AuthenticationConfiguration.builder.build()
    
    Keyless.authenticate(authenticationConfiguration: configuration) { result in
        switch result {
        case .success(let authenticationSuccess):
            print("authenticationDidFinish:  \(authenticationSuccess.token)")
        case .failure(let error):
            switch error.kind {
            case .userError(let userError):
                switch userError {
                case .faceNotMatching:
                    print("Face not matching")
                case .maskDetected:
                    print("Mask detected")
                case .spoofing:
                    print("Spoofing detected")
                case .timeout:
                    print("The operation timed out")
                case .userCancelled:
                    print("The user cancelled the operation")
                case .noNetworkConnection:
                    print("No network connection available")
                case .lockout:
                    print("Your account is temporarily locked")
                }
            default:
                let code = error.code
                let message = error.message
                // here you could display a generic error popup with the error code
            }
        }
    }
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/authentication_configuration.dart';
    
    final configuration = BiomAuthConfig();
    
    try {
      final result = await Keyless.instance.authenticate(configuration);
      print("Authentication successful");
    } catch (error) {
      if (error is KeylessError) {
        switch (error.errorType) {
          case KeylessErrorType.user:
            if (error.code == KeylessErrorCase.faceNotMatching.code) {
              print("Face not matching");
            } else if (error.code == KeylessErrorCase.maskDetected.code) {
              print("Mask detected");
            } else if (error.code == KeylessErrorCase.spoofing.code) {
              print("Spoofing detected");
            } else if (error.code == KeylessErrorCase.timeout.code) {
              print("The operation timed out");
            } else if (error.code == KeylessErrorCase.userCancelled.code) {
              print("The user cancelled the operation");
            } else if (error.code == KeylessErrorCase.noNetworkConnection.code) {
              print("No network connection available");
            } else if (error.code == KeylessErrorCase.deviceTampered.code) {
              print("Device security check failed");
            } else if (error.code == KeylessErrorCase.lockout.code) {
              print("Your account is temporarily locked");
            }
            break;
          default:
            // Handle internal or integration errors
            print("Authentication failed: ${error.message} (Code: ${error.code})");
            // Here you could display a generic error popup with the error code
        }
      }
    }
    public interface EnrollmentConfigurationBuilder {
    
        public fun retrievingBackup(): EnrollmentConfigurationBuilder
    
        public fun retrievingTemporaryState(): EnrollmentConfigurationBuilder
    
        public fun savingSecret(customSecret: String): EnrollmentConfigurationBuilder
    
        public fun withBackup(backupKey: ByteArray, backupData: ByteArray): EnrollmentConfigurationBuilder
    
        public fun withDelay(cameraDelaySeconds: Int): EnrollmentConfigurationBuilder
    
        public fun withEnrollmentSelfie(): EnrollmentConfigurationBuilder
    
        public fun withLivenessSettings(
            livenessConfiguration: LivenessSettings.LivenessConfiguration,
            livenessTimeout: Int
        ): EnrollmentConfigurationBuilder
    
        public fun withOperationInfo(
            operationId: String,
            payload: String? = null,
            externalUserId: String? = null
        ): EnrollmentConfigurationBuilder
    
        public fun withPin(pin: String): EnrollmentConfigurationBuilder
    
        public fun withTemporaryState(temporaryState: String): EnrollmentConfigurationBuilder
    
        public fun build(): EnrollmentConfiguration
    }
    public protocol EnrollmentConfigurationBuilder {
    
        public func retrievingBackup() -> EnrollmentConfigurationBuilder
    
        public func retrievingTemporaryState() -> EnrollmentConfigurationBuilder
    
        public func savingSecret(_ customSecret: String) -> EnrollmentConfigurationBuilder
    
        public func withBackup(_ backup: Keyless.Backup) -> EnrollmentConfigurationBuilder
    
        public func withDelay(seconds: Int) -> EnrollmentConfigurationBuilder
    
        public func withEnrollmentSelfie() -> EnrollmentConfigurationBuilder
    
        public func withLivenessSettings(
            livenessConfiguration: Keyless.LivenessConfiguration,
            livenessTimeout: Int
        ) -> EnrollmentConfigurationBuilder
    
        public func withOperationInfo(
            id: String,
            payload: String?,
            externalUserId: String?
        ) -> EnrollmentConfigurationBuilder
    
        public func withPin(_ pin: String) -> EnrollmentConfigurationBuilder
    
        public func withTemporaryState(_ temporaryState: String) -> EnrollmentConfigurationBuilder
    
        public func build() -> Keyless.EnrollmentConfiguration
    }
    class BiomEnrollConfig {
        final String? customSecret;
        final String? temporaryState;
        final OperationInfo? operationInfo;
        final LivenessConfiguration? livenessConfiguration;
        final int? livenessTimeout;
        final bool? shouldRetrieveTemporaryState;
        final int? cameraDelaySeconds;
        final JwtSigningInfo? jwtSigningInfo;
        final DynamicLinkingInfo? dynamicLinkingInfo;
        final bool? showScreenInstructions;
        final bool? showScreenSuccessFlow;
    }
    
    // The BiomEnrollConfig can be instantiated with these optional properties.
    // example: new BiomEnrollConfig({ customSecret: 'your-secret', cameraDelaySeconds: 3 });
     class BiomEnrollConfig {
      public readonly livenessConfiguration: LivenessConfiguration;
      public readonly livenessEnvironmentAware: boolean;
      public readonly cameraDelaySeconds: number;
      public readonly generatingClientState: ClientStateType | null;
      public readonly shouldRetrieveEnrollmentFrame: boolean;
      public readonly clientState: string | null;
      public readonly customSecret: string | null;
      public readonly iamToken: string | null;
      public readonly operationInfo: OperationInfo | null;
      public readonly jwtSigningInfo: JwtSigningInfo | null;
      public readonly showSuccessFeedback: boolean;
      public readonly showFailureFeedback: boolean;
      public readonly showScreenInstructions: boolean;
     }
    
    data class EnrollmentSuccess(Delaying the Keyless evaluation/decision
    By default, our biometric decision making process initiates immediately on the camera being invoked and the camera preview showing. We believe this offers the maximum level of security in that an attacker is given no additional preparation time in front of the camera view, while at the same time we have looked to optimise the experience for genuine users i.e. delivering both approve and reject decisions in a way that feels natural and understandable to users.
    
    However, we recognise that our customers, and their users, have different contexts and preferences and therefore the cameraDelaySeconds configuration is available to specify the delay (in seconds) between when the camera preview appears, and when the liveness processing starts. In effect, no decision, whether 
    
    Please note we advise careful consideration when implementing this feature for two reasons:
    
    i) While this allows users to frame themselves and have longer to understand what is happening, is also time for any attackers to also optimise their framing. 
    ii) Implementing will ultimately mean that the "happy path" flow for all users is extended. If the delay is set for too long, some customers have noted that there is also the potential for some users to become frustrated and cancel/drop the flow. 
    
    We're happy to engage further in what the best trade-offs may be for customers, given our wide-ranging experience of assisting customers in live implementations.
        val keylessId: String,
        val customSecret: String = "",
        val signedJwt: String? = null,
        val enrollmentFrame: Bitmap? = null,
        val temporaryState: String? = null
    ) : KeylessSdkSuccess()
    public struct EnrollmentSuccess {
        public let keylessId: String?
        public let customSecret: String?
        public let enrollmentFrame: CGImage?
        public let signedJwt: String?
        public let clientState: String?
    }
    class EnrollmentSuccess {
        final String keylessId;
        final String? customSecret;
        final String? signedJwt;
        final String? clientState;
    }
    
    class EnrollmentSuccess {
      public readonly keylessId: string;
      public readonly customSecret: string;
      public readonly enrollmentFrame: string | null;
      public readonly signedJwt: string | null;
      public readonly clientState: string | null;
      }
      
    val configuration = EnrollmentConfiguration.builder.build()
    
    Keyless.enroll(
      enrollmentConfiguration = configuration,
      onCompletion = { result ->
        when (result) {
          is Keyless.KeylessResult.Success -> Log.d("KeylessSDK ", "Enroll success - userId ${result.value.keylessId}")
          is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "Enroll failure - error code ${result.error.code}")
        }
      }
    )
    let configuration = Keyless.EnrollmentConfiguration.builder.build()
    
    Keyless.enroll(
      enrollmentConfiguration: configuration,
      onCompletion: { result in
        switch result {
        case .success(let enrollmentSuccess):
            print("Enrollment finished successfully. UserID: \(enrollmentSuccess.keylessId)")
        case .failure(let error):
            print("Enrollment finished with error: \(error.message)
        }
      })
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/enrollment_configuration.dart';
    
    final configuration = BiomEnrollConfig();
    
    try {
      final result = await Keyless.instance.enroll(configuration);
      print("Enrollment finished successfully. UserID: ${result.keylessId}");
    } catch (error) {
      print("Enrollment finished with error: $error");
    }
    
    import Keyless, { BiomEnrollConfig } from '@react-native-keyless/sdk';
    
    const enrollUser = async () => {
      const enroll = new BiomEnrollConfig();
    
      const result = await Keyless.enroll(enroll);
    
      result.fold({
        onSuccess: (data) => {
          console.log(`Enrollment finished successfully. UserID: ${data.keylessId}`);
        },
        onFailure: (error) => {
          console.error('Enrollment finished with error:', error);
        },
      });
    };
    PASSIVE_STANDALONE_MEDIUM
    PASSIVE_STANDALONE_HIGH        //recommended configuration
    PASSIVE_STANDALONE_HIGHEST
    interface AuthenticationConfigurationBuilder {
    
        fun retrievingBackup(): AuthenticationConfigurationBuilder
    
        fun retrievingSecret(): AuthenticationConfigurationBuilder
    
        fun deletingSecret(): AuthenticationConfigurationBuilder
    
        fun retrievingTemporaryState(): AuthenticationConfigurationBuilder
    
        fun withDelay(cameraDelaySeconds: Int): AuthenticationConfigurationBuilder
    
         fun withLivenessSettings(
            livenessConfiguration: LivenessSettings.LivenessConfiguration,
            livenessTimeout: Int
        ): AuthenticationConfigurationBuilder
    
        fun withMessageToSign(message: String): AuthenticationConfigurationBuilder
    
        fun withOperationInfo(
            operationId: String,
            payload: String? = null,
            externalUserId: String? = null
        ): AuthenticationConfigurationBuilder
    
        fun withPin(pin: String): AuthenticationConfigurationBuilder
    
        fun withSuccessAnimation(enabled: Boolean = true): AuthenticationConfigurationBuilder
    
        fun build(): AuthenticationConfiguration
    }
    public class Builder {
    
        public func retrievingBackup() -> Builder
    
        public func retrievingSecret() -> Builder
    
        public func deletingSecret() -> Builder
    
        public func retrievingTemporaryState() -> Builder
    
        public func revokingDevice(id: String) -> Builder
    
        public func withDelay(seconds: Int) -> Builder
    
        public func withLivenessSettings(
            livenessConfiguration: LivenessConfiguration,
            livenessTimeout: Int
        ) -> Builder
    
        public func withMessageToSign(_ message: String) -> Builder
    
        public func withOperationInfo(
            id: String,
            payload: String? = nil,
            externalUserId: String? = nil
        ) -> Builder
    
        public func withPin(_ pin: String) -> Builder
    
        public func withSuccessAnimation(_ enabled: Bool) -> Builder
    
        public func build() -> AuthenticationConfiguration
    }
    class BiomAuthConfig extends AuthConfig {
        final LivenessConfiguration? livenessConfiguration;
        final int? livenessTimeout;
        final int? cameraDelaySeconds;
        final bool? shouldRetrieveTemporaryState;
        final String? b64NewDeviceData;
        final String? b64OldDeviceData;
        final String? deviceToRevoke;
        final bool? shouldRetrieveSecret;
        final bool? shouldRemovePin;
        final JwtSigningInfo? jwtSigningInfo;
        final DynamicLinkingInfo? dynamicLinkingInfo;
        final OperationInfo? operationInfo;
        final bool? showScreenSuccessFlow;
    }
    class BiomAuthConfig {
      public readonly shouldRemovePin: boolean;
      public readonly cameraDelaySeconds: number;
      public readonly showSuccessFeedback: boolean;
      public readonly shouldRetrieveSecret: boolean;
      public readonly shouldDeleteSecret: boolean;
      public readonly jwtSigningInfo: JwtSigningInfo | null;
      public readonly dynamicLinkingInfo: DynamicLinkingInfo | null;
      public readonly livenessConfiguration: LivenessConfiguration;
      public readonly livenessEnvironmentAware: boolean;
      public readonly deviceToRevoke: string | null;
      public readonly operationInfo: OperationInfo | null;
      public readonly generatingClientState: ClientStateType | null;
      public readonly shouldRetrieveAuthenticationFrame: boolean | null;
      public readonly presentationStyle: PresentationStyle;
    }
    
    data class AuthenticationSuccess(
        val customSecret: String? = null,
        val signedJwt: String? = null,
        val clientState: String? = null
    ) : KeylessSdkSuccess()
    public struct AuthenticationSuccess {
        public let customSecret: String?
        public let signedJwt: String?
        public let clientState: String?
    }
    class AuthenticationSuccess {
        final String? customSecret;
        final String? signedJwt;
        final String? temporaryState;
    }
    class AuthenticationSuccess {
      customSecret: string | null;
      signedJwt: string | null;
      clientState: string | null;
    }
    val configuration = BiomAuthConfig(presentationStyle = PresentationStyle.NO_CAMERA_PREVIEW)
    Keyless.authenticate(configuration = configuration, onCompletion = { result ->
        // Handle result
    })
    let configuration = BiomAuthConfig(presentationStyle: .noCameraPreview)
    Keyless.authenticate(configuration: configuration, onCompletion: { result in
        // Handle result
    })
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/authentication_configuration.dart';
    
    final configuration = BiomAuthConfig(presentationStyle: PresentationStyle.NO_CAMERA_PREVIEW);
    
    try {
      final result = await Keyless.instance.authenticate(configuration);
      print("Authentication success");
    } catch (error) {
      print("Authentication failure");
    }
    import Keyless, { BiomAuthConfig } from '@react-native-keyless/sdk';
    
    const authenticateUserWithNoCameraPreview = async () => {
      const configuration = new BiomAuthConfig({
          presentationStyle: PresentationStyle.NO_CAMERA_PREVIEW,
        });
    
      const result = await Keyless.authenticate(configuration);
    
      result.fold({
        onSuccess: (data) => {
          console.log('Authentication success', data);
        },
        onFailure: (error) => {
          console.error('Authentication failure:', error);
        },
      });
    };
    val configuration = AuthenticationConfiguration.builder.build()
    
    Keyless.authenticate(
        authenticationConfiguration = configuration,
        onCompletion = { result ->
            when (result) {
                is Keyless.KeylessResult.Success -> Log.d("KeylessSDK ", "Authentication success")
                is Keyless.KeylessResult.Failure -> Log.d("KeylessSDK ", "Authentication failure - error code ${result.error.code}")
            }
        }
    )
    let configuration = Keyless.AuthenticationConfiguration.builder.build()
    
    Keyless.authenticate(
        authenticationConfiguration: configuration,
        onCompletion: { result in
            switch result {
            case .success(let success):
                print("Authentication success")
            case .failure(let error):
                break
            }
        })
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/authentication_configuration.dart';
    
    final configuration = BiomAuthConfig();
    
    try {
      final result = await Keyless.instance.authenticate(configuration);
      print("Authentication success");
    } catch (error) {
      print("Authentication failure");
    }
    import Keyless, { BiomAuthConfig } from '@react-native-keyless/sdk';
    
    const authenticateUser = async () => {
      const configuration = new BiomAuthConfig();
    
      const result = await Keyless.authenticate(configuration);
    
      result.fold({
        onSuccess: (data) => {
          console.log('Authentication success', data);
        },
        onFailure: (error) => {
          console.error('Authentication failure:', error);
        },
      });
    };
    PASSIVE_STANDALONE_MEDIUM
    PASSIVE_STANDALONE_HIGH        //recommended configuration
    PASSIVE_STANDALONE_HIGHEST
    public val livenessConfiguration: LivenessSettings.LivenessConfiguration = PASSIVE_STANDALONE_HIGH,
    public val livenessEnvironmentAware: Boolean = true,
    public val operationInfo: OperationInfo?,
    public val shouldRetrieveEnrollmentFrame: Boolean = false,
    public val showSuccessFeedback: Boolean = true,
    public val showInstructionsScreen: Boolean = true,
    public val showFailureFeedback: Boolean = true,
    public val generatingClientState: ClientStateType? = null,
    public val clientState: String? = null
    )
    public let livenessEnvironmentAware: Bool
    public let operationInfo: Keyless.OperationInfo?
    public let shouldReturnEnrollmentFrame: Bool
    public let showSuccessFeedback: Bool,
    public let showInstructionsScreen: Bool,
    public let showFailureFeedback: Bool,
    public let generatingClientState: ClientStateType?,
    public let clientState: String?
    }
    PASSIVE_STANDALONE_HIGH,
    public val livenessEnvironmentAware: Boolean = true
    public val operationInfo: OperationInfo?,
    public val shouldRemovePin: Boolean = false,
    public val shouldRetrieveSecret: Boolean = false,
    public val shouldDeleteSecret: Boolean = false,
    public val showSuccessFeedback: Boolean = true,
    public val generatingClientState: ClientStateType? = null
    )
    Bool
    public let operationInfo: Keyless.OperationInfo?
    public let shouldRemovePin: Bool
    public let shouldRetrieveSecret: Bool
    public let shouldDeleteSecret: Bool
    public let showSuccessFeedback: Bool
    public let generatingClientState: ClientStateType?
    }
    new device activation
    Keyless dashboard
    the Server API

    Changelog

    For a seamless SDK integration make sure to follow the section getting started.

    You can check all the prerequisites that your app must meet for a successsful SDK integration under the prerequisites section.

    Please note releases marked as release candidate - rc are available to selected customers in advance of the official release via partners-rc cloudsmith repo. Please note regression tests and QA activity is incomplete and we strongly advise against shipping these versions into production environments.


    5.4.0

    Breaking Change

    • We updated the cloudsmith artifacts' naming convention, with a summary of the changes as follows:

      Please follow the to fetch the latest version.

    5.3.3

    Highlights

    • We have now switched the liveness environment aware configuration default to livenessEnvironmentAware = false. For more details in go to section.


    5.3.2

    Anti-Inject variant

    Only for customers using the Keyless Anti-Inject SDK variant

    • Fine-tune error error code: 40002, error message: device might not be genuine based on updated security level.

    5.3.1 iOS

    Fixes

    • Avoid stripping debug symbols in order to run on iOS simulators.

    5.3.0

    Highlights

    • The existing lockout policy is now applied for enrollment using the client state. See the page for all details.

    Fixes

    • Improved Error Handling for Spoofing errors: the SDK will return a Rejected taking into consideration internal filters. The Rejected error will take precedence over the incorrect Spoofing error.

    Anti-Inject variant

    Only for customers using the Keyless Anti-Inject SDK variant

    • Upon calling Keyless.configure you will now get an error if we detect that device is not genuine. The error is error code: 40002, error message: device might not be genuine. You shall redirect your users through a different flow since Keyless configuration will fail and it won't not possible to enroll or authenticate.


    5.2.1

    Highlights

    • Introduced a Client State type for clearer Client State handling.

      • This change means that on the , customers using this SDK version and above will be able to see the type of "Keyless Client Devices" on the individual user pages.

      • This clears up the confusion where "Devices" of the users were displayed but there was no way to distinguish bound client devices (now labelled 'Device Type: SDK) or client states stored or used by integrator to authenticate users in future using temporary or backup client states (Device Type: Temporary or Backup).

    Deprecations

    • Temporary State terminology is now deprecated in favor of Client State.

      • Flows such as Enroll from Temporary State, for example, will be deprecated having made Enroll from Client State available as a direct replacement.

      • This is to clean-up a legacy issue where the terms Temporary State and Client State were used interchangeably.


    5.2.0

    Highlights

    • UI: optionally display a chip to show a loader before an authentication. This has been added as in some instances integrators have found there is a slight delay for the camera preview to open.

      • Check the Authentication loading in the section for more details.

    Fixes

    • Fix: For the error xMinPercentage is not in valid range [0.0..1.0] - Region of interest will be full screen in case we get inconsistent values.

      • Note we believe this error was particularly prevalent on foldable devices or tablets but should be largely resolved from this release onwards.

    • Fix: update tensorflow to resolve a that was causing a crash in a very low % of cases on android devices.

    Breaking changes

    • Removed the deprecated configuration builders. We believe that all of our customers have npw already moved to the remaining configuration: SeutpConfig, EnrollConfig, AuthConfig, and DeEnrollConfig and therefore no action is required.

    • Removed deprecated UserInfo APIs.


    5.1.2 Android

    Highlights

    • 16 KB page size support -


    5.1.1

    Highlights

    Only for customers using the Keyless Anti-Inject variant for enhanced frames injection prevention

    • Build variant is available for selected customers through partners-rasp.

    • Improved initialization performance for frames injection prevention to enhance the overall experience by reducing load times.

    Fixes

    • Fix (Android): camera resolution issues on Samsung Galaxy A23


    5.1.0

    Highlights

    • Enrollment Frame: Integrators can specify whether or not to retrieve an enrollment frame, acquired during the user’s selfie capture during the enrollment or account recovery flow. See details in section.

    • Enhanced Anti-Inject: this release introduces a new option for enhanced injection attack prevention.


    5.0.5 iOS - 5.0.6 Android

    Highlights

    • Environment-Aware Liveness Detection: a new, optional check has been introduced to enhance liveness detection helping to ensure the user is in a suitable setting for verification. This feature is enabled by default. To disable it, set livenessEnvironmentAware = false in the configuration for enrollment, authentication, or de-enrollment. See details in section.

    • Face Occlusion Detection for Enrollment: during the enrollment process, the SDK now provides immediate feedback to the user if their face is partially covered. A message, "Make sure your face is clearly visible," will be displayed to guide them..

    Fixes

    • Improved Error Handling for Temporary States: the SDK now returns a more specific internal error (error code: 539) if the maximum number of temporary states is reached during an operation.

    • Added a Cancellation Button to the Camera View: an "x" button has been added to the top-left corner of the camera screen during enrollment, allowing users to safely cancel the face scan process.


    5.0.3

    Highlights

    • Enroll from photo preview: This feature introduces the ability to enroll users using a photo provided from a trusted source.

      • Developer Responsibility: You are responsible for ensuring that the photo originates from a verified and trusted source (e.g., an official identity document).

      • Security Note: Exposing this functionality to your end-users without rigorous verification of the photo's origin may introduce vulnerabilities, such as a user enrolling with another individual's facial image. Please implement appropriate safeguards.

    Enhancement: biometric processing

    • Improved biometric performance through optimizations in frame timestamp processing.


    5.0.1

    Highlights

    • New Enrollment UX: a freshened up, new User Interface has been implemented and made available.

      • It now displays real-time feedback about the quality of the processed image, allowing users to address the issues based on the messages shown during face capture.

        • Note these messages are not customizable since they are tied to Keyless image processing.

    Improved error handling and information

    Integrators familiar with our will notice that we have:

    • Added more detailed insight and guidance for the different types of errors and the relevant error code ranges for integrators to be aware of.

    • More clearly laid out the different types of User Errors with their codes & descriptions.

    • "Rejected" error - we have split out instances where Liveness could not be established into a new "Rejected" error. Previously these were all labelled as "Spoofing" errors, however we recognized that in some instances these incorrectly added a framed the authentication attempt as more malicious than was fair. Please note, the guidance remains that all modelling is in some way predictive and thefore we would still advise that "Spoofing" does not guarantee that there was malicious intent and for various reasons it's still better to assume positive intent in how you handle these with users.

    SDK Size Reduction

    Following customer feedback, we have reduced the size of the biometric libraries which has reduced both the iOS and Android SDK integration and download size. Please note that integration size can vary significantly between customers so please contact us if you have questions in this area.

    Support for iOS emulator

    The Keyless iOS SDK can now be run on an iOS emulator on both Windows PC or Mac, allowing integrators to achieve faster develop and test cycles as they make changes.

    Deprecations

    • Liveness timeout no longer has effect and will be removed in future releases.

      • This change was made given a set of comprehensive improvements to the Liveness biometrics models. We plan to support configuration options to support both faster and slower authentication and enrollment experiences in upcoming releases.

    • The mobile SDK Lockout policy, which had some reported inconsistencies, is now managed on the backend so no longer has an effect and will be removed in future releases.

    Breaking changes

    • With the new UI texts that are shown to the user have changed. If you are using text customization, please make sure to update to the identifiers from the tab SDK v5 .

    • The API showScreenSuccessFaceCapture is no longer available since enrollment no longer considers the successful face capture step.

    • The successAnimationEnabled and later showScreenSuccessFlow


    4.8.2 iOS - 4.8.3 Android

    Highlights

    • Improved biometric performance.

    • Improved error handling and information - .

    • SDK Size Reduction.

    • Support for iOS emulator.

    Improved error handling and information

    • Introduced a new "Rejected" error - we have split out instances where Liveness could not be established into a new "Rejected" error. Previously these were all labelled as "Spoofing" errors, however we recognized that in some instances these incorrectly added a framed the authentication attempt as more malicious than was fair. Please note, the guidance remains that all modelling is in some way predictive and thefore we would still advise that "Spoofing" does not guarantee that there was malicious intent and for various reasons it's still better to assume positive intent in how you handle these with users.

    SDK Size Reduction

    Following customer feedback, we have reduced the size of the biometric libraries which has reduced both the iOS and Android SDK integration and download size. Please note that integration size can vary significantly between customers so please contact us if you have questions in this area.

    Support for iOS emulator

    The Keyless iOS SDK can now be run on an iOS emulator on both Windows PC or Mac, allowing integrators to achieve faster develop and test cycles as they make changes.


    4.7.5 iOS - 4.7.6 Android

    Highlights

    • Fix: fix client state retro compatibility with version >2.0.0 of the Keyless Agent


    4.7.4

    Highlights

    • UX: optional screens - it is now possible to opt-out the optional screens you don’t wish to show -

    • Enhancement: query remaining lockout time - \

    UX: optional screens

    We understand that the experience our customers create for their end users is of the utmost importance. To provide greater flexibility, we’ve made certain screens and steps in the Authentication and Enrollment flows optional. These screens will remain visible by default, but you now have the ability to opt out of displaying those that are not relevant to your workflow. For more details, please refer to the .

    Please note that the camera view and Step 2 (“Enrollment Progress”) will remain mandatory for now, as they are critical to ensuring liveness detection and security during the capture process. However, Step 2 is highly customizable in terms of text and color scheme to align with your branding.

    In upcoming releases, we will also be adding sample code and “User Flow” guides to further empower integrators and designers in tailoring their user experience.

    Enhancement: query remaining lockout time

    Customers can configure the maximum number of errors a user can make before being locked out for a 10-minute period.

    Based on customer feedback, we’ve enhanced this feature to allow querying the remaining lockout time (in seconds) after a user has been locked out. For implementation details, please refer to the .

    This enhancement enables customers to display the remaining lockout time to users at any given moment, improving transparency and user experience.

    Bugs and Fixes

    • Fix (Android): Resolved a compatibility issue between the Compose Material library and Flutter.


    4.7.3

    • UX: SDK Theme is customizable by customer -

    • Feature: possibility to specify a path to Keyless artifacts. Artifacts are provided by Keyless by default. Artifacts path can be overridden by the customer.


    4.7.2

    • Feature: improved PSD2 compliance -

    • Feature: account recovery -

    • Fix: remove lottie dependency

    • UX: remove success animation in favor of static image


    4.7.0

    • API Surface: deprecate builder pattern in favor of optional config constructor parameters (builders are deprecated)

    • Feature: expose SDK logs to customer app -

    • Feature: expose JWT signature compatible with core backend key pairs -


    4.6.7

    • Bugfix: internal data cleanup

    • Performance improvements: avoid unnecessary assets extraction


    4.6.6

    Feature: Shared Circuits: integrators can now set the desired number of shared circuits when calling an sdk configure Fix: Liveness: update liveness setting for higher security


    4.6.5

    • Feature: enroll with Keyless from an Auth0 user’s IdToken


    4.6.3

    • Dynamic linking feature: new API exposing interface to choose one authentication method (BiomAuthConfig | PinAuthConfig)

    • UI customization: it is now possible to customize the font for Keyless SDK screens

    • UI customization: it is now possible to customize the brand color (primary accent color) for Keyless SDK screens


    From now on Client State is the preferred term and has two types: Temporary and Backup , and represents the non-pii data stored or leveraged by customer to support future authentications on Mobile or Web SDK, such as a new device activation flow.
    Removed the possibility to specify a path to Keyless artifacts introduced in version 4.7.3.
    The temporary state now displays the enrollment UI. The change means that users will experience an onboarding flow as for plain enrollment (withtout temporary state).
    • This change was made based on customer feedback in recognition of the fact that some users will be experiencing Keyless for the first time, specifically if they were enrolled via IDV Bridge.

  • The UI differences are reported in the tab SDK v5 from the UI customization section.

  • Fixes in this release

    • Added showScreenFailureFlow for consistency with the happy path showScreenSuccessFlow. By default the SDK shows an error screen for unhappy paths but the caller app can opt-out from this default behavior.

    • Return an error if camera permission is not granted. Keyless prompts the user for camera permission and now also informs the caller app that permission was not granted returning the error 30009 - camera denied .

      • See for details of this and all other errors.

  • This change was implemented based on customer feedback and done in conjunction with other changes:

    • Client side errors (example Liveness failures) now are sent to the server side and will impact the server side policy.

    • The server side policy can now be configured per customer tenant (max failed attempts, time period, suspension period). Speak to the Keyless team if you would like to review and change your lockout policy.

    field has been renamed to
    showSuccessFeedback
    .
  • The showScreenFailureFlow field has been renamed to showFailureFeedback.

  • The showScreenInstructions field has been renamed to showInstructionsScreen.

  • The enrollment progress onProgress callback is no longer available since progress is not shown with the updated UX.

  • Fix (Android): addressed biometric performance on Pixel devices.
  • Fix (Android): on premise remove validate device before authenticate

  • getting started
    Liveness Settings
    lockout policy
    Keyless dashboard
    Text UI customization
    known issue
    official docs
    Enrollment Frame
    Liveness Settings
    error handling documentation
    text section
    docs
    docs
    docs
    documentation
    documentation
    docs
    docs
    docs
    docs
    docs
    Android: from keyless-android-sdk to keyless-mobile-sdk
    
    iOS (SPM): from keyless.KeylessSDK to keyless.mobile-sdk
    
    iOS (CocoaPods): from KeylessSDK to keyless-mobile-sdk
    user errors

    1️⃣ Getting started

    In this short guide, you will learn how to integrate the Keyless SDK in your Android or iOS mobile application, and enroll and authenticate users through the Keyless platform.

    Before jumping into your code editor, make sure that you're familiar with the various components of the authentication system, and common biometic authentication flows.

    Prerequisites

    Make sure you have both required API keys, and the list of productions hosts from your Keyless contact:

    • YOUR_CLOUDSMITH_TOKEN to download the SDK from cloudsmith repository

    • KEYLESS_API_KEY to configure the mobile SDK

    • KEYLESS_HOSTS a list of node URLs. Urls in KEYLESS_HOSTS must NOT contain any trailing slashes /.

    Review the Keyless SDK requirements:

    The Keyless SDK uses

    • (API level 23) and above

    Installation

    Keyless Anti-Inject variant for enhanced frames injection prevention

    If you are using the anti-inject SDK variant with runtime application self protection - RASP, you need to:

    • Set the package repository partners-rasp-* communicated to you by our Delivery team.

    1. To allow Keyless to handle the result of , you must call Keyless from an Activity implementing .

      Extend any androidX activity that implements the interface for you, for example let your Activity extend the generic or the more widespread .

    2. If you use Proguard, add the following rules to your Proguard configuration file:

    3. In the the repositories section of the settings.gradle

    Essential configuration

    There is some essential configuration to do before you can use the Keyless SDK.

    1. Initialize the Keyless SDK in your Application class:

    If your application crashes due to java.lang.IllegalStateException: Attempting to launch an unregistered ActivityResultLauncher or while using React/Native bridges you can't receive a completion result at the end of an enrollment, it may be due to a difference between your applicationId and the namespace of your activities.

    Additional configuration

    As well as the essential configuration parameters, you can also specify the optional configuration parameters detailed in the following sections. Details of these configuration options can be summarised in the relevant sections throughout the Mobile SDK (i.e. , etc.) and you can inspect the Config class to see optional parameters (or the builder to see exposed methods). Some of the more the more generic configurations are described below for convenience.

    Shared circuits

    This is the target desired number of circuits kept on the server. To set this number use the numberOfSharedCircuits.

    Networking module

    If you need to perform network requests on behalf of the Keyless SDK, you can implement the KLNetworkingModule interface. Set the networkingModule parameter with an instance of the KLNetworkingModule:

    UI Customization

    The Keyless SDK can be customised in order to provide a more familiar UX when integrated in any custom app.

    Theme

    The SDK theme can be customized to be dark, light or system.

    Keyless will use the system defined theme by default.

    Keyless.UI.Color.sdkTheme = SdkTheme.SYSTEM
    
    // default theme is SYSTEM
    public enum class SdkTheme {
        DARK,
        LIGHT,
        SYSTEM
    }
    Keyless.UI.Color.sdkTheme = .system
    
    // default theme is system
    public enum SDKTheme {
       case dark
       case light
       case system
    }
    import Keyless, { SdkTheme } from '@react-native-keyless/sdk';
    
    const ui = new Keyless.UI();
    ui.color.sdkTheme = Keyless.SdkTheme.LIGHT;
    await 
    

    Colors

    It is possible to customize the following colors:

    • primary color This color is the one that appears most frequently across the screens and components.

    • onPrimary color The color used for elements that appear on top of the PrimaryColor, ensuring clear contrast and visibility.

    • accent color Color used in some small details of the UI (e.g. Camera Frame borders)

    Example

    Text

    It is possible to customize the following text across the Introduction, Error, Enrollment and Success screens. Live Filters cannot be customized individually but translation files are available and can be provided.\

    Introduction

    • title Should be no longer than two lines

    • description Should be no longer than four lines

    • cta Should be no more than two words

    Enrollment

    We suggest to use no more than three lines of text.

    • Instruction

      • title

      • subtitle

      • tip1 This typically suggests to stay in a well lit area

    • Error

      • title

      • subtitle

      • cta

    • Process

      • title

      • subtitle

    • Success

      • title Showed when the enrollment has been successfully completed.

      • subtitle Showed when the enrollment has been successfully completed

      • cta Button text to show on Success screen

    Authentication

    We suggest to not use more than two lines of text.

    • loading Shown before the camera opens.

    • centerFace Shown before the user positions the face.

    • scan Shown while scanning the user's face.

    • authenticate Shown while authenticating the user.

    Live Filters

    These appear dynamically on the camera preview screen during enrollment and guide the user to capture a high quality selfie are core to the product and subject to iterative change over time as we improve the product. Therefore these are not configurable individually however we do support localization – currently English and Italian are available but new ones can be created upon request.

    The example below shows a live filter appearing to help the user. The corners/frame turns to the accent color (yellow in this example) once the biometric library is satisfied that a suitable image has been captured.

    The following filters are returned though note the first two ("It's a bit dark, move closer to the light" and "The image is too bright, adjust lightning") are due to be added in Q4 2025.

    Dynamic Linking

    We suggest to not use more than two lines of text.

    • Authentication.PayloadConfirmation

      • title

      • subtitle

      • denyCta

    Example

    Optional Screens

    It is possible to show or hide some of the Enrollment and Authentication steps above by setting to false the following fields.

    The default value for all of them is true which means that the step is shown.

    Enrollment

    To toggle certain screens in Enrollment flow, use the following fields of a BiomEnrollConfig:

    • showInstructionsScreen: affects the Instruction screen (by default true)

    • showSuccessFeedback: affects the Success screen (by default true)

    • showFailureFeedback

    Example

    Authentication

    Use the following fields of a BiomAuthConfig:

    • showSuccessFeedback: affects the Step3

    Example

    Fonts

    It is possible to customize the font.

    • Android: requires an android.graphics.Typeface object.

    • iOS: requires a String, the name of the font.

    The custom font must be set as soon as available, a good moment to do so is before calling Keyless.configure

    Example

    Haptic feedback

    Haptic feedback provides subtle vibration responses to user interactions.

    It is possible to disable/enable haptic feedback. By default, haptic feedback is enabled (true).

    Example

    Theme

    The SDK theme can be customized to be dark, light or system.

    Keyless will use the system defined theme by default.

    Colors

    It is possible to customize the following colors:

  • AndroidX

  • The Keyless SDK uses

    • iOS 13

    • Swift 5.1

    • Cocoapods 1.15.2

    Set up a physical iOS device for running your app and enable the following permissions:

    • Enable Camera permissions: add the Privacy - Camera Usage Description key in your project’s Info.plist by adding the following (in XCode under Project > Info):

    • Enable background processing: necessary to synchronize Keyless data. You can find a comprehensive guide in the . In a nutshell here is what you need to do: enable the Background processing mode under Signing & Capabilities/Background Modes. Then, add the following in your project’s Info.plit (in XCode, under the key Permitted background task scheduler identifiers):

    The Keyless SDK uses

    • Flutter 3.0 or higher

    • Dart 3.0 or higher

    • All requirements from both Android and iOS platforms

    Set up the required permissions as described in the Android and iOS tabs.

    The Keyless RN SDK requires:

    • React 18.2.0 or higher

    • React Native 0.71.0 or higher

    Additionally, ensure you meet the native requirements for each platform:

    • Android: Minimum SDK 23 (Android 6.0), Kotlin 2.2.0 or higher.

    • iOS: Target version 13.0 or higher.

    Add into your app assets the license file provided to you by our Delivery team.
    file of your Android application, add the following snippet, replacing
    YOUR_CLOUDSMITH_TOKEN
    with the CloudSmith token provided to you by Keyless.
  • In the dependencies block of your project build.gradle file, typically app/build.gradle, add:

  • In the android block of your project build.gradle file, typically app/build.gradle, make sure you have the following options:

  • We use Cloudsmith to distribute our artifacts. Cloudsmith works as a Swift Package Registry. In order to use packages from a registry, one has to set up the environment for that specific registry and possibly log in to it.

    When working with Swift Projects, registries can be configured locally to the project. Unfortunately, though, Xcode does not support local configuration of registries, thus they have to be configured globally, for the editor to be able to see them.

    It's best to run all the following commands from terminal with Xcode closed

    To configure Keyless Cloudsmith Package Registry, you can run the following commands in your terminal

    You shall replace <repo> with the name of the repo that was assigned to you by our delivery team. You will also be provided with a token to put in place of <your-api-token> to access that repo.

    Since you are adding this registry as global, it is important to provide the scope keyless for Keyless Package registry.

    These commands add the following to your ~/.swiftpm/configurations/registries.json

    Once you have this setup, you can open Xcode, go to File → Add Package Dependencies and a wizard will open to prompt the insertion of the package.

    In the top-right bar, you can search for the package, e.g. keyless.mobile-sdk, and if everything is setup correctly, the package should appear in the center column. You can add the package to the project and proper targets and you are ready to go.

    Optional - Test release candidates

    We provide our partners the chance to test Release candidates before they receive the final approval of our whole QA process. Since release candidates are stored in a separate repo and Xcode does not cope well with Semantic versioning with build numbers (which our release candidates use), we suggest to:

    1. Manually download the Package from Cloudsmith, using your assigned token

    2. Extract the archive. It should unpack in a folder called KeylessSDKPackage

    3. Drag-and-drop the extracted folder inside your Xcode project, exactly under the root of the project file.

    Once dropped inside your project, if you already had a Keyless SDK Package configured with the first method, this is going to be substituted by the local one seamlessly. If not, then just add the KeylessSDK library to your target from the project settings.

    Since Cocoapods is in maintenance mode, this method is going to be discontinued. Please integrate Keyless via SPM

    1. Create a Podfile if you don’t already have one

    2. Add the keyless-mobile-sdk pod to your Podfile

    3. Add the following at the bottom of your Podfile

    4. Install the pods.

    1. Add the Keyless SDK repository

    2. Add the Keyless SDK dependency to your pubspec.yaml:

    3. Follow the installation steps in both the Android and iOS tabs to set up the native SDKs in their respective platforms.

    4. Run flutter pub get to download the dependencies:

    You also need to bridge the native SDKs that are used by the flutter SDK. You can do that in the android and ios sections of your flutter project.

    Android

    Add the following line in the root build.gradle:

    Where YOUR_CUSTOM_TOKEN should be replaced with the cloudsmith token provided by the Keyless integration support team.

    In the example repo you can see that we are using a file to simulate env properties containing the cloudsmith token. You will read the following snippet:

    You can read the cloudsmith token from a file cloudsmith.properties that you need to create at the root project directory. File content shall be the following: cloudsmithToken=YOUR_CUSTOM_TOKEN

    Then, open your Android.manifest file and add the following lines inside the <application> tag:

    iOS

    Make sure to target at least iOS 13 in your project.

    Open your PodFile and add the following line:

    Where YOUR_CUSTOM_TOKEN should be replaced with the cloudsmith token provided by the Keyless integration support team.

    In the example repo you can see that we are using a file to simulate env properties containing the cloudsmith token. You will read the following snippet:

    File content shall be the following: cloudsmithToken=YOUR_CUSTOM_TOKEN

    Then, navigate to your Info.plist file, typically located in a folder called with your project name (e.g. Runner).

    Add the following permissions declaration to this file (if not present already):

    1. Configure Access to the Private Registry

    Create a .npmrc file in the root of your project. This file will tell your package manager where to find the Keyless SDK and how to authenticate. Add the following lines:

    Note: Replace YOUR_CLOUDSMITH_TOKEN with the actual token provided to you.

    2. Add the Keyless SDK Dependency

    Run one of the following commands in your project's terminal to install the latest version of the SDK.

    After installing the package, follow the setup steps for each native platform below.


    Android Setup

    1. Add the Maven Repository: Add the Keyless Maven repository to your root build.gradle file:

      Note: Replace YOUR_CLOUDSMITH_TOKEN with your token.

    2. Verify Kotlin Version: The Keyless SDK requires a minimum Kotlin version of 2.2.0. Depending on your React Native project's configuration, this is specified in one of two files.

      • If your project uses settings.gradle for plugins (modern setup): Open your android/settings.gradle file and ensure the version for org.jetbrains.kotlin.android is 2.2.0 or higher.


    iOS Setup

    1. Target iOS 13 or Higher: In your ios/Podfile, ensure the platform version is set to 13.0 or higher: platform :ios, '13.0'.

    2. Add the Private Spec Repo: Open your ios/Podfile and add the Keyless Cocoapods source at the top of the file:

      Note: Replace YOUR_CLOUDSMITH_TOKEN with your token.

    In such case, the Keyless.initialize(...) accepts a second parameter which is the namespace you are currently using for your Activities. Providing this parameter should fix the issue.

    Example: namespace: my.app.test applicationId: my.app.test.extra

    Since we only check that the namespace of the Activity contains the namespace provided as input, .app or my.app are also valid inputs.

    1. Add your application to the Manifest

    2. Configure the Keyless SDK from your MainActivity, ViewModel or any class you use to communicate with Keyless. Note that configure is asynchronous so wait for the completion callback before calling the next Keyless APIs

      The configure method requires a SetupConfig as parameter, as well as the KEYLESS_API_KEY and KEYLESS_HOSTS you received from Keyless. You should listen to the result as follows:

    1. Create an instance object of Keyless.SetupConfiguration and pass it to the Keyless.configure method, typically this is done in your app’s application(:didFinishLaunchingWithOptions: method:).

    1. Initialize the Keyless SDK in your Application class:

    2. Add your application to the Manifest

    3. Configure the Keyless SDK from your MainActivity, ViewModel or any class you use to communicate with Keyless. Note that configure is asynchronous so wait for the completion callback before calling the next Keyless APIs

      The configure method requires a SetupConfiguration as parameter, as well as the KEYLESS_API_KEY and KEYLESS_HOSTS you received from Keyless. You should listen to the result as follows:

    1. Create an instance object of Keyless.SetupConfiguration and pass it to the Keyless.configure method, typically this is done in your app’s application(:didFinishLaunchingWithOptions: method:).

    1. Import the required packages:

    2. Configure the Keyless SDK in your app's initialization code:

    3. Make sure to set up the required permissions in both Android and iOS platforms as described in their respective tabs.

    1. Initialize the Native SDK for Android: Open your MainApplication.kt (or .java) file and add the call to KeylessSDKModule.initialize() inside the onCreate method.

    2. Configure the Keyless SDK: This is best done once when your application starts, after the native SDK has been initialized.

      Call the configure method with your credentials. The method returns a result object that you can handle using the fold method.

    Android 6.0
    Kotlin 1.9.25
    Gradle 8.7
    registerForActivityResult
    ActivityResultCaller
    ComponentActivity
    AppCompatActivity
    Logging
    Liveness Settings
    Android Gradle Plugin 8.3.0

    tip2 This typically suggests to remove any glasses or eyewear

  • tip3 This typically suggests to be alone in the frame

  • continueCta

  • success Shown when the user has been authenticated successfully.

    approveCta

    : affects the
    Error screen
    (by default
    true
    )
    primary color This color is the one that appears most frequently across the screens and components.
  • onPrimary color The color used for elements that appear on top of the PrimaryColor, ensuring clear contrast and visibility.

  • Example

    Text

    It is possible to customize the following text:

    Enrollment

    We suggest to not use more than three lines of text.

    • Step0 (information screen before enrollment)

      • title

      • description

      • prerequisiteCenterFace

      • prerequisiteDirectLook

      • prerequisiteWellLitArea

      • prerequisiteRemoveEyeWear

      • continueCta

      • closeScreenButtonContentDescription (the content description of the close button for accessibility)

      • prerequisiteAloneInPhoto deprecated (will no longer be shown)

    • Step1

      • text1 Showed before the user face is framed.

    • step1Success

      • message Showed after the user face is framed with a successful animation.

    • Step2

      • text1 Showed after the user face has been framed and the progress is between 0% and 33%.

      • text2 Showed after the user face has been framed and the progress is between 34% and 66%.

      • text3 Showed after the user face has been framed and the progress is between 67% and 100%.

      • subtitle1 Showed during the entire enrollment progress

    • Step3

      • text1 Showed when the enrollment has been successfully completed.

      • subtitle1 Showed when the enrollment has been successfully completed

    Authentication

    We suggest to not use more than two lines of text.

    • Step1

      • text1 Showed before the user face is framed.

    • Step2

      • text1 Showed after the user face has been framed.

    • Step3

      • text1

        • Showed when the authentication has been successfully completed.

    Dynamic Linking

    We suggest to not use more than two lines of text.

    • Authentication.PayloadConfirmation

      • title

      • subtitle

      • denyCta

      • approveCta

    Example

    Optional Screens

    It is possible to show or hide some of the Enrollment and Authentication steps above by setting to false the following fields.

    The default value for all of them is true which means that the step is shown.

    Enrollment

    Use the following fields of a BiomEnrollConfig:

    • showScreenInstructions: affects the Step0

    • showScreenSuccessFaceCapture: affects the Step1Success

    • showScreenSuccessFlow: affects the Step3

    Example

    Authentication

    Use the following fields of a BiomAuthConfig:

    • showScreenSuccessFlow: affects the Step3

    Example

    Fonts

    It is possible to customize the font.

    • Android: requires an android.graphics.Typeface object.

    • iOS: requires a String, the name of the font.

    The custom font must be set as soon as available, a good moment to do so is before calling Keyless.configure

    Example

    Keyless
    .updateUI
    (ui);
    // default theme is SYSTEM
    export enum SdkTheme {
    SYSTEM = 'SYSTEM',
    LIGHT = 'LIGHT',
    DARK = 'DARK',
    }
    Keyless.UI.Color.primary = 0xFF1833B8
    Keyless.UI.Color.onPrimary = 0xFFFED900
    Keyless.UI.Color.accent = 0xFFFED900
    Keyless.UI.Color.primary = .magenta
    Keyless.UI.Color.onPrimary = .cyan
    Keyless.UI.Color.accent = .yellow
    const ui = new Keyless.UI();
    ui.color.accent = '#1833B8';
    ui.color.primary ='#FFFFFF';
    ui.color.accent = '#FED900';
    await Keyless.updateUI(ui);
    dependencies {
    // ...
    
    implementation 'io.keyless:keyless-mobile-sdk:+'
    }
    android {
        // ...
    
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    
        // add the following only if you're using Kotlin
        kotlinOptions {
            jvmTarget = "1.8"
        }
    }
    swift package-registry set --global --scope keyless https://swift.cloudsmith.io/keyless/<repo>/
    swift package-registry login https://swift.cloudsmith.io/keyless/<repo>/ --token <your-api-token>
    $ cd your-project-directory
    $ pod init
    echo '${CLOUDSMITH_TOKEN}' | dart pub token add https://dart.cloudsmith.io/keyless/flutter/
    dart pub add keyless_flutter_sdk:PACKAGE_VERSION --hosted-url https://dart.cloudsmith.io/keyless/flutter/
    flutter pub get
    @react-native-keyless:registry=https://npm.cloudsmith.io/keyless/partners-rc/
    //npm.cloudsmith.io/keyless/partners/:_authToken=YOUR_CLOUDSMITH_TOKEN
    # Using npm
    npm install @react-native-keyless/sdk
    
    # Using yarn
    yarn add @react-native-keyless/sdk
    <application
        ...
        android:name=".MainApplication"
        ...
    </application>
    let setupConfig = SetupConfig(
            apiKey: "KEYLESS_API_KEY",
            hosts: ["KEYLESS_HOSTS"]
        )
    
    if let error = Keyless.configure(configuration: setupConfig) {
        print("Keyless.Configure failed with error: \(error)")
    }
    // MainApplication
    override fun onCreate() {
        super.onCreate()
        // Initialize Keyless
        Keyless.initialize(this)
    }
    <application
        ...
        android:name=".MainApplication"
        ...
    </application>
    let setupConfiguration = Keyless.SetupConfiguration.builder
        .withApiKey("KEYLESS_API_KEY")
        .withHosts(["KEYLESS_HOSTS"])
        .build()
    
    if let error = Keyless.configure(configuration: setupConfiguration) {
        print("Keyless.Configure failed with error: \(error)")
    }
    import 'package:keyless_flutter_sdk/keyless.dart';
    import 'package:keyless_flutter_sdk/models/configurations/setup_configuration.dart';
    final setupConfiguration = SetupConfiguration(
        apiKey: "KEYLESS_API_KEY",
        hosts: ["KEYLESS_HOSTS"],
    );
    
    try {
        await Keyless.instance.configure(setupConfiguration);
        print("Keyless SDK configured successfully");
        // Keyless is ready, you can now call
        // enroll/authenticate/deEnroll ...
    } catch (error) {
        print("Failed to configure Keyless SDK: $error");
        // Handle error
    }
    // In MainApplication.kt
    import io.reactnative.keyless.sdk.KeylessSDKModule // <-- Add this import
    
    override fun onCreate() {
        super.onCreate()
        // Initialize Keyless
        KeylessSDKModule.initialize(application = this)
        // ... rest of your onCreate method
    }
    # Keyless Proguard
    -keep class io.keyless.sdk.** {*;}
    -keepclassmembers class io.keyless.sdk.** {*;}
    // MainApplication
    override fun onCreate() {
        super.onCreate()
        // Initialize Keyless
        Keyless.initialize(this)
    }
    public interface KLNetworkingModule {
        public fun sendHTTPRequest(
            baseUrl: String,
            path: String,
            method: String,
            headers: Map<String, String>,
            body: String
        ): KLHTTPResponse
    
        public data class KLHTTPResponse(
            val errorCode: Int,
            val httpCode: Int,
            val responseBody: String
        )
    }
    public protocol KLNetworkingModule {
        func sendHTTPRequest (
            host: String,
            url : String,
            method: String,
            headers : [String: String],
            body : String) -> KLHTTPResponse
    }
    
    public struct KLHTTPResponse {
        var errorCode: Int
        var httpCode: Int
        var responseBody : String
        public init(errorCode: Int, httpCode: Int, responseBody: String) {
            self.errorCode = errorCode
            self.httpCode = httpCode
            self.responseBody = responseBody
        }
    }
    dependencyResolutionManagement {
        repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
        repositories {
            google()
            mavenCentral()
            maven {
                url "https://dl.cloudsmith.io/YOUR_CLOUDSMITH_TOKEN/keyless/partners/maven/"
            }
        }
    }
      // MainApplication
      override fun onCreate() {
          super.onCreate()
          // Initialize Keyless
          Keyless.initialize(this, "my.app.test")
      }
    Keyless.UI.Color.primary = 0xFF1833B8
    Keyless.UI.Color.onPrimary = 0xFFFED900
    Keyless.UI.Color.primary = .magenta
    Keyless.UI.Color.onPrimary = .cyan
    final uiCustomization = UICustomization(
        primaryColor: 0xFF1833B8,
        onPrimaryColor: 0xFFFED900,
    );
    
    // Pass the UI customization when configuring the SDK
    final setupConfiguration = SetupConfiguration(
        apiKey: "YOUR_API_KEY",
        hosts: ["YOUR_HOSTS"],
        uiCustomization: uiCustomization
    );
    // Custom text for enrollment
    
    // prelimiar information for enrollment
    Keyless.UI.Text.Enrollment.Step0.title = "(custom) Enroll your face"
    Keyless.UI.Text.Enrollment.Step0.description = "(custom) On the next screen, we will take a photo of your face to create your account."
    
    Keyless.UI.Text.Enrollment.Step0.prerequisiteCenterFace = "(custom) Center your face in the frame"
    Keyless.UI.Text.Enrollment.Step0.prerequisiteDirectLook = "(custom) Look directly at the screen"
    Keyless.UI.Text.Enrollment.Step0.prerequisiteWellLitArea = "(custom) Ensure you are in a well-lit area"
    Keyless.UI.Text.Enrollment.Step0.prerequisiteRemoveEyeWear = "(custom) Remove any eyewear or hats"
    Keyless.UI.Text.Enrollment.Step0.continueCta = "(custom) Continue"
    
    // remaining steps of the enrollment
    Keyless.UI.Text.Enrollment.Step1.text1 = "(custom) Put your face within the frame"
    Keyless.UI.Text.Enrollment.Step1Success.message = "(custom) Photo captured successfully"
    
    Keyless.UI.Text.Enrollment.Step2.text1 = "(custom 1) Enrolling, please wait…"
    Keyless.UI.Text.Enrollment.Step2.text2 = "(custom 2) Enrolling, please wait…"
    Keyless.UI.Text.Enrollment.Step2.text3 = "(custom 3) Enrolling, please wait…"
    Keyless.UI.Text.Enrollment.Step3.text1 = "(custom) Keyless account created successfully!"
    
    // Custom text for authentication
    Keyless.UI.Text.Authentication.Step1.text1 = "(custom) Please look into the camera"
    Keyless.UI.Text.Authentication.Step2.text1 = "(custom) Communicating with the Keyless network"
    Keyless.UI.Text.Authentication.Step3.text1 = "(custom) Approved"
    
    // Custom text for dynamic linking
    Keyless.UI.Text.Authentication.PayloadConfirmation.title = "(custom) Authentication request"
    Keyless.UI.Text.Authentication.PayloadConfirmation.subtitle = "(custom) Please, approve the payment to complete the process"
    Keyless.UI.Text.Authentication.PayloadConfirmation.denyCta = "(custom) Deny"
    Keyless.UI.Text.Authentication.PayloadConfirmation.approveCta = "(custom) Approve"
    // Custom text for enrollment
    
    // prelimiar information for enrollment
    Keyless.UI.Text.Enrollment.Step0.title = "(custom) Enroll your face"
    Keyless.UI.Text.Enrollment.Step0.description = "(custom) On the next screen, we will take a photo of your face to create your account."
    
    Keyless.UI.Text.Enrollment.Step0.prerequisiteCenterFace = "(custom) Center your face in the frame"
    Keyless.UI.Text.Enrollment.Step0.prerequisiteDirectLook = "(custom) Look directly at the screen"
    Keyless.UI.Text.Enrollment.Step0.prerequisiteWellLitArea = "(custom) Ensure you are in a well-lit area"
    Keyless.UI.Text.Enrollment.Step0.prerequisiteRemoveEyeWear = "(custom) Remove any eyewear or hats"
    Keyless.UI.Text.Enrollment.Step0.continueCta = "(custom) Continue"
    
    // remaining steps of the enrollment
    Keyless.UI.Text.Enrollment.Step1.text1 = "(custom) Put your face within the frame"
    Keyless.UI.Text.Enrollment.Step1Success.message = "(custom) Position your face within the frame"
    
    Keyless.UI.Text.Enrollment.Step2.text1 = "(custom 1) Enrolling, please wait…"
    Keyless.UI.Text.Enrollment.Step2.text2 = "(custom 2) Enrolling, please wait…"
    Keyless.UI.Text.Enrollment.Step2.text3 = "(custom 3) Enrolling, please wait…"
    Keyless.UI.Text.Enrollment.Step3.text1 = "(custom) Keyless account created successfully!"
    
    // Custom text for authentication
    Keyless.UI.Text.Authentication.Step1.text1 = "(custom) Please look into the camera"
    Keyless.UI.Text.Authentication.Step2.text1 = "(custom) Communicating with the Keyless network"
    Keyless.UI.Text.Authentication.Step3.text1 = "(custom) Approved"
    
    // Custom text for dynamic linking
    Keyless.UI.Text.Authentication.PayloadConfirmation.title = "(custom) Authentication request"
    Keyless.UI.Text.Authentication.PayloadConfirmation.subtitle = "(custom) Please, approve the payment to complete the process"
    Keyless.UI.Text.Authentication.PayloadConfirmation.denyCta = "(custom) Deny"
    Keyless.UI.Text.Authentication.PayloadConfirmation.approveCta = "(custom) Approve"
    final uiCustomization = UICustomization(
        // Enrollment Step 0
        enrollmentStep0Title: "(custom) Enroll your face",
        enrollmentStep0Description: "(custom) On the next screen, we will take a photo of your face to create your account.",
        enrollmentStep0PrerequisiteCenterFace: "(custom) Center your face in the frame",
        enrollmentStep0PrerequisiteDirectLook: "(custom) Look directly at the screen",
        enrollmentStep0PrerequisiteWellLitArea: "(custom) Ensure you are in a well-lit area",
        enrollmentStep0PrerequisiteRemoveEyeWear: "(custom) Remove any eyewear or hats",
        enrollmentStep0ContinueCta: "(custom) Continue",
        
        // Enrollment Steps
        enrollmentStep1Text: "(custom) Put your face within the frame",
        enrollmentStep1SuccessText: "(custom) Photo captured successfully",
        
        enrollmentStep2Text1: "(custom 1) Enrolling, please wait…",
        enrollmentStep2Text2: "(custom 2) Enrolling, please wait…",
        enrollmentStep2Text3: "(custom 3) Enrolling, please wait…",
        enrollmentStep2Subtitle: "(custom) Processing your enrollment",
        
        enrollmentStep3Text: "(custom) Keyless account created successfully!",
        enrollmentStep3Subtitle: "(custom) You're all set!",
        
        // Authentication Steps
        authenticationStep1: "(custom) Please look into the camera",
        authenticationStep2: "(custom) Communicating with the Keyless network",
        authenticationStep3: "(custom) Approved",
    );
    
    // Pass the UI customization when configuring the SDK
    final setupConfiguration = SetupConfiguration(
        apiKey: "YOUR_API_KEY",
        hosts: ["YOUR_HOSTS"],
        uiCustomization: uiCustomization
    );
    val biomEnrollConfig = BiomEnrollConfig(
        showScreenInstructions = false,
        showScreenSuccessFaceCapture = false,
        showScreenSuccessFlow = false
    )
    
    Keyless.enroll(
        configuration = biomEnrollConfig, 
        onCompletion = { /* handle completion */ }
    )
    let biomEnrollConfig = BiomEnrollConfig(
        showScreenSuccessFlow: false,
        showScreenSuccessFaceCapture: false,
        showScreenInstructions: false
    )
            
    Keyless.enroll(
        configuration: biomEnrollConfig,
        onCompletion: { _ in /* handle completion */ }
    )
    val biomAuthConfig = BiomAuthConfig(
        showScreenSuccessFlow = false
    )
    
    Keyless.authenticate(
        configuration = biomAuthConfig,
        onCompletion = { /* handle completion */ }
    )
    let biomAuthConfig = BiomAuthConfig(
        showScreenSuccessFlow: false
    )
            
    Keyless.authenticate(
        configuration: biomAuthConfig,
        onCompletion: { _ in /* handle completion */ }
    )
    Keyless.UI.Font.customFont: Typeface = Typeface.SERIF
    Keyless.UI.Font.customFont: String = "Serif"
    final uiCustomization = UICustomization(
        customFont: "Serif"
    );
    // Custom text for introduction
    Keyless.UI.Text.Introduction.title = "(custom) Keyless Secure Authentication"
    Keyless.UI.Text.Introduction.description = "(custom) Hereon, you will start face scanning..."
    Keyless.UI.Text.Introduction.cta = "(custom) Continue"
    
    // Custom text for enrollment
    
    // prelimiar information for enrollment
    Keyless.UI.Text.Enrollment.Instruction.title = "(custom) Enroll your face"
    Keyless.UI.Text.Enrollment.Instruction.description = "(custom) On the next screen, we will take a photo of your face to create your account."
    Keyless.UI.Text.Enrollment.Instruction.tip1 = "(custom) Center your face in the frame"
    Keyless.UI.Text.Enrollment.Instruction.tip2 = "(custom) Look directly at the screen"
    Keyless.UI.Text.Enrollment.Instruction.tip3 = "(custom) Ensure you are in a well-lit area"
    Keyless.UI.Text.Enrollment.Instruction.continueCta = "(custom) Continue"
    
    // Enrollment loading screen
    Keyless.UI.Text.Enrollment.Process.title = "(custom 1) Hold on a few seconds"
    Keyless.UI.Text.Enrollment.Process.subtitle = "(custom 2) We're creating your private key"
    
    // Error screen
    Keyless.UI.Text.Enrollment.Error.title = "(custom) Something went wrong"
    Keyless.UI.Text.Enrollment.Error.subtitle = "(custom) Please try again"
    Keyless.UI.Text.Enrollment.Error.cta = "(custom) Continue"
    
    // Success screen
    Keyless.UI.Text.Enrollment.Success.title = "(custom) Keyless account created successfully!"
    Keyless.UI.Text.Enrollment.Success.subtitle = "(custom) Keyless account created successfully!"
    Keyless.UI.Text.Enrollment.Success.cta = "(custom) Keyless account created successfully!"
    
    // Custom text for authentication
    Keyless.UI.Text.Authentication.Step1.text1 = "(custom) Please look into the camera"
    Keyless.UI.Text.Authentication.Step2.text1 = "(custom) Communicating with the Keyless network"
    Keyless.UI.Text.Authentication.Step3.text1 = "(custom) Approved"
    
    // Custom text for dynamic linking
    Keyless.UI.Text.Authentication.PayloadConfirmation.title = "(custom) Authentication request"
    Keyless.UI.Text.Authentication.PayloadConfirmation.subtitle = "(custom) Please, approve the payment to complete the process"
    Keyless.UI.Text.Authentication.PayloadConfirmation.denyCta = "(custom) Deny"
    Keyless.UI.Text.Authentication.PayloadConfirmation.approveCta = "(custom) Approve"
    
    // Custom text for introduction
    Keyless.UI.Text.Introduction.title = "(custom) Keyless Secure Authentication"
    Keyless.UI.Text.Introduction.description = "(custom) Hereon, you will start face scanning..."
    Keyless.UI.Text.Introduction.cta = "(custom) Continue"
    
    // Custom text for enrollment
    
    // Prelimiar information for enrollment
    Keyless.UI.Text.Enrollment.Instruction.title = "(custom) Enroll your face"
    Keyless.UI.Text.Enrollment.Instruction.description = "(custom) On the next screen, we will take a photo of your face to create your account."
    Keyless.UI.Text.Enrollment.Instruction.tip1 = "(custom) Center your face in the frame"
    Keyless.UI.Text.Enrollment.Instruction.tip2 = "(custom) Look directly at the screen"
    Keyless.UI.Text.Enrollment.Instruction.tip3 = "(custom) Ensure you are in a well-lit area"
    Keyless.UI.Text.Enrollment.Instruction.continueCta = "(custom) Continue"
    
    // Enrollment loading screen
    Keyless.UI.Text.Enrollment.Process.title = "(custom 1) Hold on a few seconds"
    Keyless.UI.Text.Enrollment.Process.subtitle = "(custom 2) We're creating your private key"
    
    // Error screen
    Keyless.UI.Text.Enrollment.Error.title = "(custom) Something went wrong"
    Keyless.UI.Text.Enrollment.Error.subtitle = "(custom) Please try again"
    Keyless.UI.Text.Enrollment.Error.cta = "(custom) Continue"
    
    // Success screen
    Keyless.UI.Text.Enrollment.Success.title = "(custom) Keyless account created successfully!"
    Keyless.UI.Text.Enrollment.Success.subtitle = "(custom) Keyless account created successfully!"
    Keyless.UI.Text.Enrollment.Success.cta = "(custom) Keyless account created successfully!"
    
    // Custom text for authentication
    Keyless.UI.Text.Authentication.Step1.text1 = "(custom) Please look into the camera"
    Keyless.UI.Text.Authentication.Step2.text1 = "(custom) Communicating with the Keyless network"
    Keyless.UI.Text.Authentication.Step3.text1 = "(custom) Approved"
    
    // Custom text for dynamic linking
    Keyless.UI.Text.Authentication.PayloadConfirmation.title = "(custom) Authentication request"
    Keyless.UI.Text.Authentication.PayloadConfirmation.subtitle = "(custom) Please, approve the payment to complete the process"
    Keyless.UI.Text.Authentication.PayloadConfirmation.denyCta = "(custom) Deny"
    Keyless.UI.Text.Authentication.PayloadConfirmation.approveCta = "(custom) Approve"
    import Keyless from '@react-native-keyless/sdk';
    import Keyless from '@react-native-keyless/sdk';
    
    const ui = new Keyless.UI();
        // Custom text for introduction
        ui.text.introduction.title = '(custom) Keyless Secure Authentication';
        ui.text.introduction.description = '(custom) Hereon, you will start face scanning...';
        ui.text.introduction.cta = '(custom) Continue';
    
        // Custom text for enrollment
    
        // Prelimiar information for enrollment
        ui.text.enrollment.instruction.title = '(custom) Enroll your face';
        ui.text.enrollment.instruction.tip1 = '(custom) Center your face in the frame';
        ui.text.enrollment.instruction.tip2 = '(custom) Look directly at the screen';
        ui.text.enrollment.instruction.tip3 = '(custom) Ensure you are in a well-lit area';
        ui.text.enrollment.instruction.continueCta = '(custom) Continue';
        
        // Enrollment loading screen
        ui.text.enrollment.process.title = '(custom) Hold on a few seconds';
        ui.text.enrollment.process.subtitle = "(custom) We're creating your private key";
        
        // Error screen
        ui.text.enrollment.error.title = '(custom) Something went wrong';
        ui.text.enrollment.error.subtitle = '(custom) Please try again';
        ui.text.enrollment.error.cta = '(custom) Continue';
        
        // Success screen
        ui.text.enrollment.success.title = '(custom) Keyless account created successfully!';
        ui.text.enrollment.success.subtitle = '(custom) Keyless account created successfully!';
        ui.text.enrollment.success.cta = '(custom) Keyless account created successfully!';
    
        // Custom text for authentication
        ui.text.authentication.centerFace = '(custom) Please look into the camera';
        ui.text.authentication.scan = '(custom) Communicating with the Keyless network';
        ui.text.authentication.authenticate = '(custom) Approved';
        ui.text.authentication.success = '(custom) Approved';
        ui.text.authentication.loading = '(custom) Loading';
    
        // Custom text for dynamic linking
        ui.text.authentication.payloadConfirmation.title = '(custom) Authentication request';
        ui.text.authentication.payloadConfirmation.subtitle = '(custom) Please, approve the payment to complete the process';
        ui.text.authentication.payloadConfirmation.denyCta = '(custom) Deny';
        ui.text.authentication.payloadConfirmation.approveCta = '(custom) Approve';
    
        await Keyless.updateUI(ui);
    // To hide all optional screens
    val biomEnrollConfig = BiomEnrollConfig(
        showInstructionsScreen = false,
        showSuccessFeedback = false,
        showFailureFeedback = false
    )
    
    Keyless.enroll(
        configuration = biomEnrollConfig, 
        onCompletion = { /* handle completion */ }
    )
    // To hide all optional screens
    let biomEnrollConfig = BiomEnrollConfig(
        showInstructionsScreen: false,
        showSuccessFeedback: false,
        showFailureFeedback: false
    )
            
    Keyless.enroll(
        configuration: biomEnrollConfig,
        onCompletion: { _ in /* handle completion */ }
    )
    import Keyless, { BiomEnrollConfig } from '@react-native-keyless/sdk';
    
    const enrollConfig = new BiomEnrollConfig({
      showScreenInstructions: false,
      showSuccessFeedback: false,
      showFailureFeedback: false
    });
    
    const result = await Keyless.enroll(enrollConfig);
    val biomAuthConfig = BiomAuthConfig(
        showSuccessFeedback = false
    )
    
    Keyless.authenticate(
        configuration = biomAuthConfig,
        onCompletion = { /* handle completion */ }
    )
    let biomAuthConfig = BiomAuthConfig(
        showSuccessFeedback: false
    )
            
    Keyless.authenticate(
        configuration: biomAuthConfig,
        onCompletion: { _ in /* handle completion */ }
    )
    import Keyless, { BiomAuthConfig } from '@react-native-keyless/sdk';
    
    const config = new BiomAuthConfig({
      showSuccessFeedback: false,
    });
    
     const result = await Keyless.enroll(config);
    Keyless.UI.Font.customFont: Typeface = Typeface.SERIF
    Keyless.UI.Font.customFont: String = "Serif"
    Keyless.UI.hapticFeedbackEnabled = true
    Keyless.UI.hapticFeedbackEnabled = false
    Keyless.UI.Color.sdkTheme = SdkTheme.SYSTEM
    
    // default theme is SYSTEM
    public enum class SdkTheme {
        DARK,
        LIGHT,
        SYSTEM
    }
    Keyless.UI.Color.sdkTheme = .system
    
    // default theme is system
    public enum SDKTheme {
       case dark
       case light
       case system
    }

    If your project uses build.gradle for plugins (older setup): Open your root android/build.gradle file. The kotlinVersion variable is defined in the ext block and then used in the dependencies block. Please check if kotlin version is 2.2.0 or higher.

    Enable Camera Permissions: Open your ios/Info.plist file and add the camera usage description.

  • Install Native Dependencies: Navigate to the ios directory and run pod install.

  • apple documentation
    <key>NSCameraUsageDescription</key>
    <string>Keyless needs access to your camera to enroll and authenticate you. Keyless cannot be used without your camera. Please allow camera permissions.</string>
    <key>NSCameraUsageDescription</key>
    <key>BGTaskSchedulerPermittedIdentifiers</key>
    <array>
        <string>YOUR APP IDENTIFIER</string>
    </array>
    // android/build.gradle
    buildscript {
        ext {
            // ... other variables
            kotlinVersion = "2.2.0" // ✅ 1. Define version here (must be 2.2.0 or higher)
        }
        repositories {
            google()
            mavenCentral()
        }
        dependencies {
            // ... other classpaths
    
            classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") // ✅ 2. The variable is used on this line
        }
    }
    <key>NSCameraUsageDescription</key>
    <string>This app uses the camera to scan face.</string>
    cd ios && pod install
     {
      "authentication" : {
        "swift.cloudsmith.io" : {
          "loginAPIPath" : "/keyless/<repo>",
          "type" : "token"
        }
      },
      "registries" : {
        "keyless" : {
          "supportsAvailability" : false,
          "url" : "https://swift.cloudsmith.io/keyless/<repo>/"
        }
      "version" : 1
    }
    # This is the Keyless repository for partners
    source 'https://dl.cloudsmith.io/YOUR_CLOUDSMITH_TOKEN/keyless/partners/cocoapods/index.git'
    
    target 'MyApp' do
        use_frameworks!
    
        # Add the Keyless pod
        pod 'keyless-mobile-sdk'
    end
    post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
        config.build_settings['ENABLE_BITCODE'] = 'NO'
        end
    end
    end
    $ pod install
    
    Analyzing dependencies
    Cloning spec repo `cloudsmith-YOUR_CLOUDSMITH_TOKEN-keyless-partners-cocoapods-index` from `https://dl.cloudsmith.io/YOUR_CLOUDSMITH_TOKEN/keyless/partners/cocoapods/index.git`
    allprojects {
        repositories {
            google()
            mavenCentral()
            maven {
                setUrl("https://dl.cloudsmith.io/YOUR_CUSTOM_TOKEN/keyless/partners/maven/")
            }
        }
    }  
    allprojects {
        repositories {
            google()
            mavenCentral()
            maven {
                setUrl("https://dl.cloudsmith.io/$cloudsmithToken/keyless/partners/maven/")
            }
        }
    }
            <provider
                android:name="androidx.startup.InitializationProvider"
                android:authorities="${applicationId}.androidx-startup"
                android:exported="false"
                tools:node="merge">
     
                <meta-data
                    android:name="io.keyless.fluttersdk.KeylessInitializer"
                    android:value="androidx.startup" />
            </provider>
    source 'https://dl.cloudsmith.io/YOUR_CUSTOM_TOKEN/keyless/partners/cocoapods/index.git'
    source 'https://dl.cloudsmith.io/'+ ENV['cloudsmithToken'] +'/keyless/partners/cocoapods/index.git'
    <key>NSCameraUsageDescription</key> 
    <string>Keyless needs access to your camera to enroll and authenticate you. Keyless cannot be used without your camera. Please allow camera permissions.</string> 
    <key>NSMicrophoneUsageDescription</key> 
    <string>Keyless needs access to your camera to enroll and authenticate you. Keyless cannot be used without your camera. Please allow camera permissions.</string>   
    allprojects {
        repositories {
            google()
            mavenCentral()
            maven {
                url "https://dl.cloudsmith.io/YOUR_CLOUDSMITH_TOKEN/keyless/partners/maven/"
            }
        }
    }
    // android/settings.gradle
    pluginManagement {
        plugins {
            // ... other plugins
            id "org.jetbrains.kotlin.android" version "2.2.0" apply false // ✅ Must be 2.2.0 or higher
        }
    }
    # Add the Keyless source at the top
    source 'https://dl.cloudsmith.io/YOUR_CLOUDSMITH_TOKEN/keyless/partners/cocoapods/index.git'
    source 'https://cdn.cocoapods.org/'
    val setupConfig = SetupConfig(
            apiKey = "KEYLESS_API_KEY",
            hosts = listOf("KEYLESS_HOSTS")
        )
    
     Keyless.configure(setupConfig) { result ->
        when (result) {
            is Keyless.KeylessResult.Success -> {
                Log.d("KeylessSDK", "configure success")
                // Keyless is ready, you can now call
                // enroll/authenticate/deEnroll ...
            }
            is Keyless.KeylessResult.Failure ->{
                Log.d("KeylessSDK", "configure error")
                // Inspect result.error for more info
             }
         }
     }
    val setupConfiguration = SetupConfiguration.builder
        .withApiKey("KEYLESS_API_KEY")
        .withHosts(listOf("KEYLESS_HOSTS"))
        .build()
    
     Keyless.configure(setupConfiguration) { result ->
        when (result) {
            is Keyless.KeylessResult.Success -> {
                Log.d("KeylessSDK", "configure success")
                // Keyless is ready, you can now call
                // enroll/authenticate/deEnroll ...
            }
            is Keyless.KeylessResult.Failure ->{
                Log.d("KeylessSDK", "configure error")
                // Inspect result.error for more info
             }
         }
     }
    import Keyless, { SetupConfig } from '@react-native-keyless/sdk';
    
    
    const config = new SetupConfig({
      // Make sure to store your keys securely
      apiKey: 'KEYLESS_API_KEY',
      hosts: ['KEYLESS_HOSTS'],
    });
    
    const result = await Keyless.configure(config);
    
    result.fold({
      onSuccess: data => {
        console.log('Keyless SDK configured successfully:', data);
        // Keyless is ready, you can now call other methods
      },
      onFailure: error => {
        console.error('Failed to configure Keyless SDK:', error);
      },
    });