# iOS Liveness SDK \*New

Our iOS 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, run:

```sh
pod 'YouverifyLivenessSDKCompat'
```

## Usage

{% stepper %}
{% step %}

### Import the package

```swift
import YouverifyLivenessSDK
```

{% endstep %}

{% step %}

### Initialize the SDK

```swift
private var yvLiveness = YVLiveness(...options)
```

> For a list of the valid options, check the Options section below.
> {% endstep %}

{% step %}

### Start the liveness process

Start with tasks provided during initialization (or pass an array of tasks to override):

```swift
yvLiveness.startSDK(tasks: tasks)
// add the liveness view controller to the current view controller
DispatchQueue.main.async {
    self.addLivenessViewController()
}
```

Example with an explicit task array:

```swift
let livenessTasks = [
    TaskProperties(task:
        YVTask.completeTheCircle(CompleteTheCircleTask(difficulty: .medium))
    )
]

yvLiveness.startSDK(tasks: livenessTasks)
```

{% endstep %}

{% step %}

### Add and remove the liveness view controller

```swift
private var livenessViewController: YVLivenessViewController?

private func addLivenessViewController() {
    guard livenessViewController == nil else { return } // Prevent duplicate adds
    let vc = YVLivenessViewController(sdk: yvLiveness)
    vc.delegate = self
    
    addChild(vc)
    
    view.addSubview(vc.view)
    vc.didMove(toParent: self)
    livenessViewController = vc
}

private func removeLivenessViewController() {
    guard let vc = livenessViewController else { return }
    vc.willMove(toParent: nil)
    vc.view.removeFromSuperview()
    vc.removeFromParent()
    livenessViewController = nil
}
```

{% endstep %}

{% step %}

### Handle closing via delegate

Assign your view controller to the `YVLivenessViewDelegate` to handle closing:

```swift
extension MainViewController: YVLivenessViewDelegate {
    func closeModal() {
        DispatchQueue.main.async {
            self.removeLivenessViewController()
        }
    }
}
```

{% endstep %}
{% endstepper %}

## Full Example

```swift
import YouverifyLivenessSDK

class MainViewController: UIViewController {
    var yvLiveness: YVLiveness?
    
    private var livenessViewController: YVLivenessViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        initializeSDK()
        
        // Create a vertical stack view
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 10
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // Create buttons
        let completeCircleButton = createButton(title: "Complete the Circle", action: #selector(completeTheCircleTapped))
        
        // Add buttons to stack view
        stackView.addArrangedSubview(completeCircleButton)
        
        // Add stack view to the main view
        view.addSubview(stackView)
        
        // Set Auto Layout constraints
        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
        ])
    }
    
    private func initializeSDK() {
        // start loading...
        ApiClient.shared.generateSessionId(publicMerchantID: publicMerchantID,
                                               apiToken: apiToken) { [weak self] result in
            switch result {
            case .success(let sessionId):
                // Now get token
                ApiClient.shared.generateSessionToken(publicMerchantID: publicMerchantID,
                                                      deviceCorrelationId: sessionId,
                                                      apiToken: apiToken) { result in
                    switch result {
                    case .success(let authToken):
                        // end loading
                        DispatchQueue.main.async {
                            self?.yvLiveness = YVLiveness(
                                publicKey: publicMerchantID,
                                user: YVLivenessUser(firstName: "John"),
                                onSuccess: { data in
                                    print("The success data is \(data)")
                                },
                                onFailure: { errorData in
                                    if let error = errorData.error {
                                        if error.key == "invalid_or_expired_session" {
                                            print("Session expired, retrying...")
                                            self?.initializeSDK()
                                        } else if error.key == "session_token_error" {
                                            print("Session token error, retrying...")
                                            self?.initializeSDK()
                                        }
                                    }
                                },
                                sessionId: sessionId,
                                sessionToken: authToken
                            )
                        }
                    case .failure(let error):
                        print("Token failed:", error)
                    }
                }
            case .failure(let error):
                print("Session failed:", error)
            }
        }
    }
    
    private func addLivenessViewController() {
        guard livenessViewController == nil else { return } // Prevent duplicate adds
        guard let yvLiveness = yvLiveness else { return }
        let vc = YVLivenessViewController(sdk: yvLiveness)
        vc.delegate = self
        
        addChild(vc)
        
        view.addSubview(vc.view)
        vc.didMove(toParent: self)
        livenessViewController = vc
    }

    private func removeLivenessViewController() {
        guard let vc = livenessViewController else { return }
        vc.willMove(toParent: nil)
        vc.view.removeFromSuperview()
        vc.removeFromParent()
        livenessViewController = nil
    }
    
    // Helper function to create buttons with actions
    private func createButton(title: String, action: Selector) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.addTarget(self, action: action, for: .touchUpInside)
        return button
    }
    
    private func startLiveness(with tasks: [TaskProperties]) {
        guard let yvLiveness = yvLiveness else { return }
        yvLiveness.startSDK(tasks: tasks)
        DispatchQueue.main.async {
            self.addLivenessViewController()
        }
    }
    
    @objc private func completeTheCircleTapped() {
        startLiveness(with: [
            TaskProperties(task: .completeTheCircle(CompleteTheCircleTask(difficulty: .medium)))
        ])
    }
}

extension MainViewController: YVLivenessViewDelegate {
    func closeModal() {
        DispatchQueue.main.async {
            self.removeLivenessViewController()
        }
    }
}
```

### Breaking change: session token generation now external

* 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.

## Base URL Configuration

All API endpoints use the following base URL:

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. Your backend should make the following request:

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

Headers:

* Content-Type: "application/json"
* Token: "YOUR\_API\_KEY"

Request Body:

* publicMerchantID: "your\_public\_merchant\_id",
* metadata": \[:]

Response:

```swift
[
  "sessionId": "generated_session_id_here"
]
```

## Session Token Generation

Additionally, you need to generate a sessionToken for liveness verification.

Endpoint: `POST /v2/api/identity/sdk/liveness/token`

Headers:

* Content-Type: "application/json",
* Token: "YOUR\_API\_KEY"

Request Body:

* publicMerchantID: "your\_public\_merchant\_id",
* deviceCorrelationId: "unique\_device\_identifier"

Response:

```
[
  "authToken": "generated_auth_token_here"
]
```

The authToken from the response should be passed as `sessionToken` to the SDK constructor.

## Complete Integration Flow

* Generate Session ID: Call your backend to generate `sessionId` using the session generation endpoint.
* Generate Session Token: Call your backend to generate `sessionToken` using the liveness token endpoint.
* Initialize SDK: Pass both `sessionId` and `sessionToken` to the SDK constructor.
* SDK Validation: The SDK validates the `sessionId` before initialization.
* Error Handling: If validation fails, `onFailure` is called with key `invalid_or_expired_session` and `session_token_error` for both.
* Success: Upon successful initialization, the SDK uses the `sessionToken` for liveness verification.

### Error Keys

* 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

### Retry Logic

* If session validation fails, generate a new sessionId and retry.
* If liveness fails, users may retry while the current sessionId remains valid.
* If the sessionId expires, create a new session and restart the entire process.

## Token generation

Here's an example of the `ApiClient` class housing the `generateSessionId` and the `generateSessionToken`.

```swift
enum Endpoint {
    case generateSessionId
    case generateSessionToken
    
    var url: URL {
        switch self {
        case .generateSessionId:
            return URL(string: "https://api.sandbox.youverify.co/v2/api/identity/sdk/session/generate")!
            
        case .generateSessionToken:
            return URL(string: "https://api.sandbox.youverify.co/v2/api/identity/sdk/liveness/token")!
        }
    }
}

class ApiClient {
    
    static let shared = ApiClient()
    private init() {}
    
    private let jsonDecoder = JSONDecoder()
    
    // Generic request
    func post<T: Codable>(to endpoint: Endpoint,
                          apiToken: String,
                          body: [String: Any],
                          completion: @escaping (Result<T, Error>) -> Void) {
        
        var request = URLRequest(url: endpoint.url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(apiToken, forHTTPHeaderField: "Token")
        
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: body)
        } catch {
            completion(.failure(error))
            return
        }
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            
            if let error = error {
                completion(.failure(error))
                return
            }
        
            guard let data = data else {
                completion(.failure(URLError(.badServerResponse)))
                return
            }
            
            do {
                let model = try self.jsonDecoder.decode(T.self, from: data)
                completion(.success(model))
            } catch {
                completion(.failure(error))
            }
            
        }.resume()
    }
}

extension ApiClient {
    
    func generateSessionId(publicMerchantID: String,
                           apiToken: String,
                           completion: @escaping (Result<String, Error>) -> Void) {
        
        let body: [String: Any] = [
            "publicMerchantID": publicMerchantID,
            "metadata": [:]
        ]
        
        post(to: .generateSessionId, apiToken: apiToken, body: body) { (result: Result<SessionIdResponse, Error>) in
            switch result {
            case .success(let response):
                completion(.success(response.data.sessionId))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
    
    
    func generateSessionToken(publicMerchantID: String,
                              deviceCorrelationId: String,
                              apiToken: String,
                              completion: @escaping (Result<String, Error>) -> Void) {
        
        let body: [String: Any] = [
            "publicMerchantID": publicMerchantID,
            "deviceCorrelationId": deviceCorrelationId
        ]
        
        post(to: .generateSessionToken, apiToken: apiToken, body: body) { (result: Result<AuthTokenResponse, Error>) in
            switch result {
            case .success(let response):
                completion(.success(response.data.authToken))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

```

## Options

| Option                      | Type           | Required       | Description                                                                                                                          | Default Value | Possible Values            |
| --------------------------- | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------- | -------------------------- |
| `publicKey`                 | String         | No             | Your Youverify Public Merchant Key                                                                                                   | nil           | Valid Youverify Public Key |
| `sandboxEnvironment`        | Bool           | No             | Sets whether session should run in sandbox or live mode                                                                              | `true`        | `true`, `false`            |
| `tasks`                     | Array          | No             | Sets tasks that need to be performed for liveness to be confirmed                                                                    | nil           | See tasks section          |
| `user`                      | YVLivenessUser | No             | Sets details of user for which liveness check is being performed                                                                     | nil           | See nested options below   |
| `user.firstName`            | String         | Yes            | First name of user                                                                                                                   | -             | Any string                 |
| `user.lastName`             | String         | No             | Last name of user                                                                                                                    | nil           | Any string                 |
| `user.email`                | String         | No             | Email of user                                                                                                                        | nil           | Any string                 |
| `branding`                  | Branding       | No             | Customizes UI to fit your brand                                                                                                      | nil           | See nested options below   |
| `branding.name`             | String         | No             | The name of the brand                                                                                                                | nil           | Any string                 |
| `branding.color`            | String         | No             | Sets your branding color                                                                                                             | nil           | Valid hex string           |
| `branding.logo`             | String         | No             | Sets your logo                                                                                                                       | nil           | Valid image link           |
| `branding.logoAlt`          | String         | No             | Alternative text for the brand logo                                                                                                  | "Youverify"   | Any string                 |
| `branding.hideLogoOnMobile` | Bool           | No             | Hides logo in mobile webview                                                                                                         | `false`       | `true`, `false`            |
| `branding.showPoweredBy`    | Bool           | No             | Displays "Powered by" footer text                                                                                                    | `false`       | `true`, `false`            |
| `branding.poweredByText`    | String         | No             | Customizes the "Powered by" text                                                                                                     | "Powered by"  | Any string                 |
| `branding.poweredByLogo`    | String         | No             | Customizes the "Powered by" logo                                                                                                     | nil           | Valid image link           |
| `allowAudio`                | Bool           | 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                                                                           | nil           | Any valid function         |
| `onSuccess`                 | Function       | No             | Callback function that gets triggered when all tasks have been completed and passed. Called with completion data (see Liveness Data) | nil           | Any valid function         |
| `onFailure`                 | Function       | No             | Callback function that gets triggered when at least one task fails. Called with completion data (see Liveness Data)                  | nil           | Any valid function         |
| `sessionId`                 | String         | Yes (required) | ID generated by your backend using your API key. Validated before SDK init and attached to submissions                               | -             | Any valid session ID       |
| `sessionToken`              | String         | Yes (required) | Token generated by your backend for liveness verification                                                                            | -             | Any valid session token    |

## Tasks

A task is a series of instructions for users to follow to confirm liveness. We aim to frequently add to this list.

### Complete The Circle

User passes task by completing imaginary circle with head movement.

#### CompleteTheCircleTask option

| Option       | Type           | Required | Description                                                    | Default Value | Possible Values             |
| ------------ | -------------- | -------- | -------------------------------------------------------------- | ------------- | --------------------------- |
| `difficulty` | TaskDifficulty | No       | Sets difficulty of task                                        | `.medium`     | `.easy`, `.medium`, `.hard` |
| `timeout`    | Number         | No       | Sets time in milliseconds after which task automatically fails | nil           | Any number in milliseconds  |

### 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`.

#### YesOrNoTask option

| Option                   | Type           | Required | Description                                                                            | Default Value | Possible Values             |
| ------------------------ | -------------- | -------- | -------------------------------------------------------------------------------------- | ------------- | --------------------------- |
| `difficulty`             | TaskDifficulty | No       | Sets difficulty of task                                                                | `.medium`     | `.easy`, `.medium`, `.hard` |
| `timeout`                | Number         | No       | Sets time in milliseconds after which task automatically fails                         | nil           | Any number in milliseconds  |
| `questions`              | Array          | No       | A set of questions to ask user                                                         | nil           | 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`       | Bool           | Yes      | Answer to the question                                                                 | -             | `true`, `false`             |
| `questions.errorMessage` | String         | No       | Error message to display if user gets question wrong                                   | nil           | Any string                  |

### Motions

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

#### MotionsTaskClass option

| Option       | Type           | Required | Description                                                    | Default Value | Possible Values             |
| ------------ | -------------- | -------- | -------------------------------------------------------------- | ------------- | --------------------------- |
| `difficulty` | TaskDifficulty | No       | Sets difficulty of task                                        | `.medium`     | `.easy`, `.medium`, `.hard` |
| `timeout`    | TimeInterval   | No       | Sets time in milliseconds after which task automatically fails | nil           | Any number in milliseconds  |
| `maxNods`    | Int            | No       | Maximum amount of nods a user is asked to perform              | 5             | Any number                  |
| `maxBlinks`  | Int            | No       | Maximum amount of nods a user is asked to perform              | 5             | Any number                  |

### Blink

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

#### BlinkTaskClass option

| Option       | Type           | Required | Description                                                    | Default Value | Possible Values             |
| ------------ | -------------- | -------- | -------------------------------------------------------------- | ------------- | --------------------------- |
| `difficulty` | TaskDifficulty | No       | Sets difficulty of task                                        | `.medium`     | `.easy`, `.medium`, `.hard` |
| `timeout`    | TimeInterval   | No       | Sets time in milliseconds after which task automatically fails | nil           | Any number in milliseconds  |
| `maxBlinks`  | Int            | No       | Maximum amount of nods a user is asked to perform              | 5             | Any number                  |

### TaskProperties

User passes in the class and preferred settings for each task.

| Option    | Type         | Required | Description                                                    | Default Value | Possible Values                                                  |
| --------- | ------------ | -------- | -------------------------------------------------------------- | ------------- | ---------------------------------------------------------------- |
| `task`    | YVTask       | Yes      | Id of task                                                     | -             | `YVTask.completeTheCircle(Task_class eg. CompleteTheCircleTask)` |
| `timeout` | TimeInterval | No       | Sets time in milliseconds after which task automatically fails | nil           | Any number in milliseconds                                       |

## Liveness 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`       | Bool            | Indicator on whether liveness check passed or failed                                                                             |
| `data.metadata`     | 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/)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://doc.youverify.co/know-your-customer-services-kyc/sdk/ios-sdk/ios-liveness-sdk-new.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
