# Android Liveness SDK \*New

Our Android SDK enables seamless integration of real-time liveness detection into your mobile applications. To get started with our SDK, follow the guide below:

## Installation

To install, add the following line to your app's `build.gradle` file:

```kts
implementation("co.youverify:liveness-sdk-android:<version>")
```

Add this configuration to your app module's `build.gradle.kts` file:

```kts
defaultConfig {
    ndk {
        abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86"))
    }
}
```

## Usage

Create a Composable. We will call ours `LivenessComponent`.

Create the liveness SDK controller inside the composable, like so:

```kts
val livenessController = remember { YVLivenessSDKController(livenessConfig) }
```

> For a list of the valid config options, check [this](#options) out.

Pass the controller to the `YVLivenessSDK` UI Composable like so:

```kts
YVLivenessSDK(livenessController)
```

You can start the liveness process with the following function:

```kts
livenessController.start()
```

The start function also accepts a list of [task options](#tasks) as parameter `tasks`, example:

```kts
livenessController.start(tasks = listOf(
    MotionTaskOptions(options)
))
```

> The supplied tasks override the one provided during initialization through `YVLivenessConfig`.

Full example:

```kts
@Composable
fun LivenessComponent() {
    val livenessConfig = YVLivenessConfig(
        user = SDKUser(
            firstName = "John",
            lastName = "Doe"
        ),
    )

    val livenessController = remember { YVLivenessSDKController(livenessConfig) }

    fun startMotionsTask() {
        livenessController.start(tasks = listOf(
            MotionTaskOptions()
        ))
    }

    YourAppTheme {
        YVLivenessSDK(livenessController)
    }
}
```

Now, you can call your composable from your Activity.

## Options

| Option                   | Type     | Required | Description                                                                                                                        | Default Value | Possible Values            |
| ------------------------ | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------------------------- |
| `publicKey`              | String   | No       | Your Youverify Public Merchant Key                                                                                                 | null          | Valid Youverify Public Key |
| `sandboxEnvironment`     | Boolean  | No       | Sets whether session should run in sandbox or live mode                                                                            | `true`        | `true`, `false`            |
| `tasks`                  | List     | No       | Sets tasks that need to be performed for liveness to be confirmed                                                                  | null          | See [tasks](#tasks)        |
| `user`                   | SDKUser  | No       | Sets details of user for which liveness check is being performed                                                                   | null          | See nested options below   |
| `user.firstName`         | String   | Yes      | First name of user                                                                                                                 | -             | Any string                 |
| `user.lastName`          | String   | No       | Last name of user                                                                                                                  | null          | Any string                 |
| `user.email`             | String   | No       | Email of user                                                                                                                      | null          | Any string                 |
| `branding`               | Branding | No       | Customizes UI to fit your brand                                                                                                    | null          | See nested options below   |
| `branding.color`         | String   | No       | Sets your branding color                                                                                                           | null          | Valid hex or RGB string    |
| `branding.logo`          | String   | No       | Sets your logo                                                                                                                     | null          | Valid image link           |
| `branding.name`          | String   | No       | Sets your branding name                                                                                                            | null          | Any string                 |
| `branding.hideLogo`      | Boolean  | No       | Hides logo                                                                                                                         | `false`       | `true`, `false`            |
| `branding.showPoweredBy` | Boolean  | No       | Shows powered by logo                                                                                                              | `false`       | `true`, `false`            |
| `branding.poweredByText` | String   | No       | Customizes the "Powered By" text                                                                                                   | "Powered by"  | Any string                 |
| `branding.poweredByLogo` | String   | No       | Sets your powered by logo                                                                                                          | null          | Valid image link           |
| `allowAudio`             | Boolean  | No       | Sets whether to narrate information to user during tasks                                                                           | `false`       | `true`, `false`            |
| `onClose`                | Function | No       | Callback function that gets triggered when modal is closed                                                                         | null          | Any valid function         |
| `onSuccess`              | Function | No       | Callback function that gets triggered when all tasks have been completed and passed. Called with completion [data](#callback-data) | null          | Any valid function         |
| `onFailure`              | Function | No       | Callback function that gets triggered when at least one task fails. Called with completion [data](#callback-data)                  | null          | Any valid function         |
| `sessionId`              | String   | Yes      | ID generated by your backend using your API key. Validated before SDK init and attached to submissions                             | -             | Any valid session ID       |
| `sessionToken`           | String   | Yes      | Token generated by your backend for liveness verification                                                                          | -             | Any valid session token    |

{% hint style="info" %}
Latest Changes

* The SDK no longer generates session tokens internally.
* Partners must call their backend to generate both sessionId and sessionToken and pass them to the SDK via the respective options.
  {% endhint %}

### Base URL Configuration

All API endpoints use the following base URLs:

* Sandbox base URL:

```
https://api.sandbox.youverify.co
```

* Live base URL:

```
https://api.youverify.co
```

### Session ID generation

Before initializing the SDK, you must generate a sessionId by calling your backend API.

**Endpoint:** `POST /v2/api/identity/sdk/session/generate`

#### Create a request

```kotlin
val request = Request.Builder()
    .url("$baseURL/v2/api/identity/sdk/session/generate")
    .addHeader("Token", apiToken)
    .post(bodyJson.toRequestBody("application/json".toMediaType()))
    .build()
```

#### Create a request body

```kotlin
val bodyJson = JSONObject().apply {
    put("publicMerchantID", publicMerchantId)
    put("metadata", JSONObject()) // whatever metadata you want to pass in
}.toString()
```

#### JSON response

```json
{
  "sessionId": "generated_session_id_here"
}
```

#### Error handling

The `sessionId` should be passed to the SDK constructor.

{% stepper %}
{% step %}

### Complete Integration Flow — Step 1

Generate Session ID: Call your backend to generate `sessionId` using the session generation endpoint.
{% endstep %}

{% step %}

### Complete Integration Flow — Step 2

Generate Session Token: Call your backend to generate `sessionToken` using the liveness token endpoint.
{% endstep %}

{% step %}

### Complete Integration Flow — Step 3

Initialize SDK: Pass both `sessionId` and `sessionToken` to the SDK config.
{% endstep %}

{% step %}

### Complete Integration Flow — Step 4

SDK Validation: The SDK validates the `sessionId` before initialization.
{% endstep %}

{% step %}

### Complete Integration Flow — Step 5

Error Handling: If validation fails, `onFailure` is called with key `invalid_or_expired_session` and `session_token_error` for both.
{% endstep %}

{% step %}

### Complete Integration Flow — Step 6

Success: Upon successful initialization, the SDK uses the `sessionToken` for liveness verification.
{% endstep %}
{% endstepper %}

### Error Keys

<details>

<summary>List of error keys</summary>

* `invalid_or_expired_session`: Returned when the `sessionId` is invalid or expired
* `session_token_error`: Returned when there's an issue with the `sessionToken` during liveness verification

</details>

### Complete Example Implementation

Defining the `LivenessSessionManagement` view model and the \`LivenessSessionRepository:

```kotlin
class LivenessSessionRepository {

    private val client = OkHttpClient()

    suspend fun generateSessionId(
        publicMerchantId: String,
        apiToken: String
    ): String = withContext(Dispatchers.IO) {

        val bodyJson = JSONObject().apply {
            put("publicMerchantID", publicMerchantId)
            put("metadata", JSONObject())
        }.toString()

        val request = Request.Builder()
            .url("https://api.sandbox.youverify.co/v2/api/identity/sdk/session/generate")
            .addHeader("Token", apiToken)
            .post(bodyJson.toRequestBody("application/json".toMediaType()))
            .build()

        val response = client.newCall(request).execute()
        JSONObject(response.body.string()).getString("sessionId")
    }

    suspend fun generateSessionToken(
        publicMerchantId: String,
        deviceCorrelationId: String,
        apiToken: String
    ): String = withContext(Dispatchers.IO) {

        val bodyJson = JSONObject().apply {
            put("publicMerchantID", publicMerchantId)
            put("deviceCorrelationId", deviceCorrelationId)
        }.toString()

        val request = Request.Builder()
            .url("https://api.sandbox.youverify.co/v2/api/identity/sdk/liveness/token")
            .addHeader("Token", apiToken)
            .post(bodyJson.toRequestBody("application/json".toMediaType()))
            .build()

        val response = client.newCall(request).execute()
        JSONObject(response.body.string()).getString("authToken")
    }
}
```

```kotlin
class LivenessSessionManagement(
    private val repo: LivenessSessionRepository = LivenessSessionRepository()
): ViewModel() {
    var sessionId by mutableStateOf<String?>(null)
        private set
    var authToken by mutableStateOf<String?>(null)
        private set
    var isLivenessLoading by mutableStateOf(false)
        private set

    fun load(publicMerchantId: String, deviceCorrelationId: String, apiToken: String) {
        viewModelScope.launch {
            isLivenessLoading = true
            sessionId = null
            authToken = null

            try {
                val id = repo.generateSessionId(publicMerchantId, apiToken)
                val token = repo.generateSessionToken(publicMerchantId, deviceCorrelationId, apiToken)

                sessionId = id
                authToken = token
            } finally {
                isLivenessLoading = false
            }
        }
    }
}
```

Modify `LivenessComponent` to implement these changes.

```kotlin
@Composable
fun LivenessComponent(
    publicKey: String,
    deviceCorrelationId: String,
    apiToken: String,
    sessionViewModel: LivenessSessionManagement = viewModel()
) {
    val sessionId = sessionViewModel.sessionId
    val sessionToken = sessionViewModel.authToken
    val isLivenessLoading = sessionViewModel.isLivenessLoading

    fun initLivenessTokens() {
        sessionViewModel.load(publicKey, deviceCorrelationId, apiToken)
    }

    LaunchedEffect(Unit) {
        initLivenessTokens()
    }

    if (isLivenessLoading || sessionId == null || sessionToken == null) {
        // LoaderUI
        return
    }

    val livenessConfig = YVLivenessConfig(
        sessionId = sessionId,
        sessionToken = sessionToken,
        onSuccess = { data ->
            println("The returned data is $data")
            // Handle success here
        },
        onFailure = { data ->
            println("Liveness check failed with data $data")

            if (data?.error?.key == "invalid_or_expired_session") {
                // Session expired, generate new session and retry
                println("Session expired, retrying...")
                initLivenessTokens()
            } else if (data?.error?.key == "session_token_error") {
                // Session token error, generate new token and retry
                println("Session token error, retrying...")
                initLivenessTokens()
            }
        },
    )

    val livenessController = remember { YVLivenessSDKController(livenessConfig) }

    fun startYesOrNoTask() {
        livenessController.start(tasks = listOf(
            YesOrNoTaskOptions(questions = listOf(
                Question(
                    question = "Is Nigeria a country?",
                    errorMessage = "Read the question more carefully",
                    answer = true
                )
            ))
        ))
    }
    
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column {
            Button(onClick = { startYesOrNoTask() }) {
                Text("Start Yes or No Task")
            }
        }

        YVLivenessSDK(livenessController)
    }
}
```

{% hint style="warning" %}
Important Notes

* API Token Security: Never expose your API token in frontend code. All API calls to generate `sessionId` and `sessionToken` should be made from your secure backend.
* Device Correlation ID: The `deviceCorrelationId` should be a unique identifier for the user's device/session. This helps track and prevent abuse.
* Error Handling: Always implement proper error handling for both API calls and SDK initialization to provide a smooth user experience.
* Environment: Use `sandboxEnvironment: true` for testing and `sandboxEnvironment: false` for production.
  {% endhint %}

## Tasks

A task is a series of instructions for users to follow to confirm liveness. Find below a list of tasks.

> PS: We aim to frequently add to this list a variety of fun and yet intuitive ways of confirming liveness, so be on the lookout for more tasks!

### Complete The Circle

User passes task by completing imaginary circle with head movement.

#### CTCTaskOptions

| Option       | Type           | Required | Description                                                    | Default Value         | Possible Values                  |
| ------------ | -------------- | -------- | -------------------------------------------------------------- | --------------------- | -------------------------------- |
| `task`       | Task           | Yes      | Id of task                                                     | -                     | `Task.COMPLETE_THE_CIRCLE`       |
| `difficulty` | TaskDifficulty | No       | Sets difficulty of task                                        | TaskDifficulty.Medium | .Easy, .Medium, .Hard            |
| `timeout`    | Long           | No       | Sets time in milliseconds after which task automatically fails | `20000L`              | Any number in milliseconds(Long) |

### Yes Or No

User passes task by answering a list of arbitrary questions set by you with the tilting of the head; right for a `yes` and left for a `no`.

#### YesOrNoTaskOptions

| Option                   | Type           | Required | Description                                                                            | Default Value         | Possible Values                  |
| ------------------------ | -------------- | -------- | -------------------------------------------------------------------------------------- | --------------------- | -------------------------------- |
| `task`                   | Task           | Yes      | Id of task                                                                             | -                     | `Task.YES_OR_NO`                 |
| `difficulty`             | TaskDifficulty | No       | Sets difficulty of task                                                                | TaskDifficulty.Medium | .Easy, .Medium, .Hard            |
| `timeout`                | Long           | No       | Sets time in milliseconds after which task automatically fails                         | `20000L`              | Any number in milliseconds(Long) |
| `questions`              | List           | No       | A set of questions to ask user                                                         | null                  | See nested options below         |
| `questions.question`     | String         | Yes      | Question to ask user. Should be a `true` or `false` type question. Eg: "Are you ready" | -                     | Any string                       |
| `questions.answer`       | Boolean        | Yes      | Answer to the question                                                                 | -                     | `true`, `false`                  |
| `questions.errorMessage` | String         | No       | Error message to display if user gets question wrong                                   | undefined             | Any string                       |

### Motions

User passes task by performing random motions in random sequences, which include `nodding`, `blinking` and `opening of mouth`.

#### MotionTaskOptions

| Option       | Type           | Required | Description                                                    | Default Value         | Possible Values                  |
| ------------ | -------------- | -------- | -------------------------------------------------------------- | --------------------- | -------------------------------- |
| `task`       | Task           | Yes      | Id of task                                                     | -                     | `Task.MOTIONS`                   |
| `difficulty` | TaskDifficulty | No       | Sets difficulty of task                                        | TaskDifficulty.Medium | .Easy, .Medium, .Hard            |
| `timeout`    | Long           | No       | Sets time in milliseconds after which task automatically fails | `20000L`              | Any number in milliseconds(Long) |
| `maxNods`    | Int            | No       | Maximum amount of nods a user is asked to perform              | 5                     | Any integer                      |
| `maxBlinks`  | Int            | No       | Maximum amount of nods a user is asked to perform              | 5                     | Any integer                      |

### Blink

User passes task by blinking their eyelids based on number of times set.

#### BlinkTaskOptions

| Option       | Type           | Required | Description                                                    | Default Value         | Possible Values                  |
| ------------ | -------------- | -------- | -------------------------------------------------------------- | --------------------- | -------------------------------- |
| `task`       | Task           | Yes      | Id of task                                                     | -                     | `Task.MOTIONS`                   |
| `difficulty` | TaskDifficulty | No       | Sets difficulty of task                                        | TaskDifficulty.Medium | .Easy, .Medium, .Hard            |
| `timeout`    | Long           | No       | Sets time in milliseconds after which task automatically fails | `20000L`              | Any number in milliseconds(Long) |
| `maxBlinks`  | Int            | No       | Maximum amount of nods a user is asked to perform              | 5                     | Any integer                      |

## Callback Data

The `onSuccess` and `onFailure` callbacks (if supplied) are passed the following data:

| Option              | Type              | Description                                                                                                                       |
| ------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `data`              | LivenessData      | Data passed through callback                                                                                                      |
| `data.faceImage`    | String            | Face Image of user performing liveness check                                                                                      |
| `data.livenessClip` | String            | Video of user performing liveness check                                                                                           |
| `data.passed`       | Boolean           | Indicator on whether liveness check passed or failed                                                                              |
| `data.metadata`     | Map\<String, Any> | Metadata passed in during initialization                                                                                          |
| `data.error`        | LivenessError     | Error object containing error key(`eyes_closed`, `image_capture_failed`, `API_ERROR`) and message (only present in failure cases) |

## Credits

This SDK is developed and maintained solely by [Youverify](https://youverify.co/)
