Skip to content

Flow WebView

Description#

Flow Webview is a way to integrate the biometric service into the client-side application using a ready-made solution in the form of a WebView library.

Using this method of integration assumes obtaining a session from the created flow and subsequently redirecting the end user to our server.

Note

Currently, the integration happens entirely on the client-side. Code examples are provided below for Flutter, iOS (Swift) and Android (Kotlin).

Stages:#

1. Creating a Flow#

The first thing you need to do is to create a Flow in your personal account. To do this, go to this link.

Once the flow has been created it is necessary to use its API KEY in the second step. You can find the API KEY on the list of created flows page .

Simplified example of the table of personal account flows:
Flow name API KEY Technologies
Flow for everyone TTpme201NiUMBA... LD, F2F
Flow for individuals Efy202XKbVAWRu... LD, ED, F2F
Flow for legal entities UuuH4V1XC-M9je... LD, DR, F2F

Note

The following examples will use the first flow from the table above: "Flow for everyone".

Also, a shortened API KEY length is used for clarity. Its actual length is 47 characters or more.

2. Session Creation#

To work with the service you need to create a session. To do this, you need to send a POST-request to create a session, using the API KEY of the created flow. Read more about sessions here.

Requests to our service use only data in JSON format. Response data from the service will be presented in the same format.

Request URL:

https://kyc.biometric.kz/api/v1/flows/session/create/

Request format Request method
JSON POST

API KEY must be passed in the request body:

Field name Type Obligatorily Description
api_key String Yes API KEY of the created flow in the personal cabinet

Request examples:

curl --location --request POST 'https://kyc.biometric.kz/api/v1/flows/session/create/' \
--header 'Content-Type: application/json' \
--data-raw '{
"api_key": "YOUR_FLOW_API_KEY"
}'
import requests
import json

url = "https://kyc.biometric.kz/api/v1/flows/session/create/"

payload = json.dumps({
  "api_key": "YOUR_FLOW_API_KEY",
})
headers = {
  'Content-Type': 'application/json',
}

response = requests.request(
  "POST",
  url=url,
  headers=headers,
  data=payload,
)

print(response.json())
const apiKey = 'YOUR_FLOW_API_KEY' // flow api key

fetch('https://kyc.biometric.kz/api/v1/flows/session/create/', {
    method: 'POST',
    body: JSON.stringify({
        api_key: apiKey
    })
})
.then(response => response.json())
.then(data => {
    const { session_id, technologies } = data
})
const apiKey: string = 'YOUR_FLOW_API_KEY' // flow api key

interface SessionResponseData {
    session_id: string,
    technologies: string[]
}

fetch('https://kyc.biometric.kz/api/v1/flows/session/create/', {
    method: 'POST',
    body: JSON.stringify({
        api_key: apiKey
    })
})
.then(response => response.json())
.then(data => {
    const { session_id, technologies }: SessionResponseData = data
})

The response will be JSON with the following fields:

  • session_id - one-time session identifier to pass the flow;
  • technologies - an ordered set of flow technologies.

Response example:

{
  "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "technologies": ["LDHP", "F2F"]
}

Session lifetime

The session is valid for 30 minutes from the moment of creation. If the user does not complete the verification in time, a new session must be created.

Create sessions on your server

The session must be created on your server side — the API KEY must never be embedded in the client code of the mobile application.

3. Session Metadata#

When creating a flow, certain metadata can be included to facilitate a more flexible and dynamic process, as well as to simplify the verification procedure.

API KEY and metadata must be sent in the request body. The value for metadata is an object that includes an object with the technology name (face2face, edocument) consisting of metadata for the technology.

Field Name Type Required Description
api_key String Yes The API KEY of the created flow in the personal cabinet
metadata No Session metadata

Metadata for Face2Face technology:

Field Name Type Required Description
face2face No Metadata for Face2Face technology
photo1 String No Photo used for matching in base64 format

Metadata for E-Document technology:

Field Name Type Required Description
edocument No Metadata for E-Document technology
iin No IIN field
phone No Subject's phone number field, format: 77*********
value String No Field values
changeable Boolean No Ability for the user to change the data
skip_input Boolean No Skip user data input (Move to OTP code input step)

Interchangeable technologies

The list of interchangeable technologies (interchangeable_techs) is configured at the Flow configuration level in the personal cabinet, not in the session metadata.

Note

If metadata is absent, the flow will proceed by default.

3.1. Metadata for Face2Face#

If a user's face photo is available, it can be included in the session metadata. This helps simplify and shorten the verification process and prevent errors that might occur during user verification. The metadata field must be sent in the request body.

Important to Note

Face2Face technology metadata will only work for the following flows:

  • Liveness + Face2Face
  • E-Document + Face2Face
  • Document Recognition + Face2Face
Field Name Type Required Description
face2face No Metadata for Face2Face technology
photo1 String No Photo used for matching in base64 format

Request URL:

https://kyc.biometric.kz/api/v1/flows/session/create/

Request Format Request Method
JSON POST

Request Examples:

curl --location --request POST 'https://kyc.biometric.kz/api/v1/flows/session/create/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "api_key": "YOUR_FLOW_API_KEY",
    "metadata": {
        "face2face": {
            "photo1": "base64"
        }
    }
}'
import requests
import json
import base64

url = "https://kyc.biometric.kz/api/v1/flows/session/create/"

with open('path/to/your/image.jpg', 'rb') as image_file:
    encoded_image = base64.b64encode(image_file.read()).decode('utf-8')

payload = json.dumps({
    "api_key": "YOUR_FLOW_API_KEY",
    "metadata": {
        "face2face": {
            "photo1": encoded_image,
        },
    },
})

headers = {
    'Content-Type': 'application/json',
}

response = requests.post(url=url, headers=headers, data=payload)

print(response.json())
const apiKey = 'YOUR_FLOW_API_KEY';
const imageFilePath = 'path/to/your/image.jpg'; // Path to your image

const fs = require('fs');
const path = require('path');

const imageBase64 = fs.readFileSync(path.resolve(imageFilePath), { encoding: 'base64' });

const payload = {
    api_key: apiKey,
    metadata: {
        face2face: {
            photo1: imageBase64
        }
    }
};

fetch('https://kyc.biometric.kz/api/v1/flows/session/create/', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
    const { session_id, technologies } = data;
})

The response will be a JSON with the following fields:

  • session_id - a one-time session identifier for passing the flow;
  • technologies - an ordered set of flow technologies.

Example response:

{
  "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "technologies": ["LDHP", "F2F"]
}

3.2. Metadata for E-Document#

If user data such as IIN, phone number, or presence in the Mobile Citizen Database is available, it can be sent to the input form when passing the EDocument technology. This helps simplify and shorten the verification process and prevent errors that might occur during user data entry. The metadata field must be sent in the request body.

Note

The skip_input field defaults to false (the data entry step is not skipped). The changeable field defaults to true (the user can change the data entry field values).

Field Name Type Required Description
edocument No Metadata for E-Document technology
iin No IIN field
phone No Subject's phone number field, format: 77*********
value String No Field values
changeable Boolean No Ability for the user to change the data
skip_input Boolean No Skip user data input

Request URL:

https://kyc.biometric.kz/api/v1/flows/session/create/

Request Format Request Method
JSON POST

Case №1:

Note

Skipping the user data entry step is only possible if:
1) value for iin and phone is provided
2) changeable for iin and phone is provided with a value of false
3) skip_input is provided with a value of true

The user data entry step will be skipped. The user will be redirected to the OTP code input step from 1414.

curl --location --request POST 'https://kyc.biometric.kz/api/v1/flows/session/create/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "api_key": "YOUR_FLOW_API_KEY",
    "metadata": {
        "edocument": {
            "iin": {
                "value": "<subjects_iin>",
                "changeable": false,
            },
            "phone": {
                "value": "<subjects_phone>",
                "changeable": false,
            },
            "skip_input": true
        }
    }
}'
import requests
import json

url = "https://kyc.biometric.kz/api/v1/flows/session/create/"

payload = json.dumps({
    "api_key": "YOUR_FLOW_API_KEY",
    "metadata": {
        "edocument": {
            "iin": {
                "value": "<subjects_iin>",
                "changeable": False,
            },
            "phone": {
                "value": "<subjects_phone>",
                "changeable": False,
            },
            "skip_input": True
        },
})

headers = {
    'Content-Type': 'application/json',
}

response = requests.post(url=url, headers=headers, data=payload)

print(response.json())
const apiKey = 'YOUR_FLOW_API_KEY'; // Your flow API key
const subjectsIIN = '123456789012'; // Example user's IIN
const subjectsPhone = '77*********'; // Example user's phone number

fetch('https://kyc.biometric.kz/api/v1/flows/session/create/', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        api_key: apiKey,
        metadata: {
            edocument: {
                iin: {
                    value: subjectsIIN,
                    changeable: false,
                },
                phone: {
                    value: subjectsPhone,
                    changeable: false,
                },
                skip_input: true
            }
        }
    })
})
    .then(response => response.json())
    .then(data => {
        const { session_id, technologies } = data;
    })

Case №2:

At the data entry step, the user will see the values passed in the metadata fields, without the ability to change the IIN.

Note

The default value for changeable is true.
The default value for skip_input is false.

curl --location --request POST 'https://kyc.biometric.kz/api/v1/flows/session/create/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "api_key": "YOUR_FLOW_API_KEY",
    "metadata": {
        "edocument": {
            "iin": {
                "value": "<subjects_iin>",
                "changeable": false,
            },
            "phone": {
                "value": "<subjects_phone>",
            }
        }
    }
}'
import requests
import json

url = "https://kyc.biometric.kz/api/v1/flows/session/create/"

payload = json.dumps({
    "api_key": "YOUR_FLOW_API_KEY",
    "metadata": {
        "edocument": {
            "iin": {
                "value": "<subjects_iin>",
                "changeable": False,
            },
            "phone": {
                "value": "<subjects_phone>",
            },
        },
})

headers = {
    'Content-Type': 'application/json',
}

response = requests.post(url=url, headers=headers, data=payload)

print(response.json())
const apiKey = 'YOUR_FLOW_API_KEY'; // Your flow API key
const subjectsIIN = '123456789012'; // Example user's IIN
const subjectsPhone = '77*********'; // Example user's phone number

fetch('https://kyc.biometric.kz/api/v1/flows/session/create/', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        api_key: apiKey,
        metadata: {
            edocument: {
                iin: {
                    value: subjectsIIN,
                    changeable: false,
                },
                phone: {
                    value: subjectsPhone,
                }
            }
        }
    })
})
    .then(response => response.json())
    .then(data => {
        const { session_id, technologies } = data;
    })

Case №3:

At the data entry step, the user will see the values passed in the metadata fields, with the ability to change the IIN and phone number.

curl --location --request POST 'https://kyc.biometric.kz/api/v1/flows/session/create/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "api_key": "YOUR_FLOW_API_KEY",
    "metadata": {
        "edocument": {
            "iin": {
                "value": "<subjects_iin>",
            },
            "phone": {
                "value": "<subjects_phone>",
            }
        }
    }
}'
import requests
import json

url = "https://kyc.biometric.kz/api/v1/flows/session/create/"

payload = json.dumps({
    "api_key": "YOUR_FLOW_API_KEY",
    "metadata": {
        "edocument": {
            "iin": {
                "value": "<subjects_iin>",
            },
            "phone": {
                "value": "<subjects_phone>",
            },
        },
})

headers = {
    'Content-Type': 'application/json',
}

response = requests.post(url=url, headers=headers, data=payload)

print(response.json())
const apiKey = 'YOUR_FLOW_API_KEY'; // Your flow API key
const subjectsIIN = '123456789012'; // Example user's IIN
const subjectsPhone = '77*********'; // Example user's phone number

fetch('https://kyc.biometric.kz/api/v1/flows/session/create/', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        api_key: apiKey,
        metadata: {
            edocument: {
                iin: {
                    value: subjectsIIN,
                },
                phone: {
                    value: subjectsPhone,
                }
            }
        }
    })
})
    .then(response => response.json())
    .then(data => {
        const { session_id, technologies } = data;
    })

The response will be a JSON with the following fields:

  • session_id - a one-time session identifier for passing the flow;
  • technologies - an ordered set of flow technologies.

Example response:

{
  "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "technologies": ["LDHP", "ED", "F2F"]
}

4. Integration into the Application#

After receiving the session_id, open the following URL in the WebView:

https://remote.biometric.kz/flow/<session_id>?web_view=true

The web_view=true parameter is mandatory — it tells the application that it is running inside a WebView. In this mode:

  • browser redirects are disabled;
  • when the session finishes, the application navigates to https://remote.biometric.kz/finished;
  • the "Close" button leading to an external browser is not shown.

4.1 Required WebView settings#

For the camera and video to work correctly inside the WebView, the following settings are required:

Setting Value Why it matters
allowsInlineMediaPlayback true Video must play inline inside the WebView, not in a fullscreen player
mediaPlaybackRequiresUserGesture false Allows the camera stream to start automatically
JavaScript enabled The application is built with Vue.js
Camera access granted Biometric verification requires the camera

4.2 Code examples#

import 'package:webview_flutter/webview_flutter.dart';

class BiometricWebView extends StatefulWidget {
  final String sessionId;
  const BiometricWebView({required this.sessionId, super.key});

  @override
  State<BiometricWebView> createState() => _BiometricWebViewState();
}

class _BiometricWebViewState extends State<BiometricWebView> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onNavigationRequest: (request) {
            if (request.url.startsWith('https://remote.biometric.kz/finished')) {
              Navigator.of(context).pop(); // close the WebView
              _fetchResult();              // request the result
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
        ),
      )
      ..loadRequest(
        Uri.parse(
          'https://remote.biometric.kz/flow/${widget.sessionId}?web_view=true&locale=en',
        ),
      );
  }

  void _fetchResult() {
    // Request the result from your server using the session_id
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: WebViewWidget(controller: _controller),
    );
  }
}
import WebKit

class BiometricViewController: UIViewController, WKNavigationDelegate {
    var sessionId: String = ""
    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let config = WKWebViewConfiguration()
        // Required: play video without a user gesture
        config.allowsInlineMediaPlayback = true
        config.mediaTypesRequiringUserActionForPlayback = []

        webView = WKWebView(frame: view.bounds, configuration: config)
        webView.navigationDelegate = self
        view.addSubview(webView)

        let url = URL(string: "https://remote.biometric.kz/flow/\(sessionId)?web_view=true&locale=en")!
        webView.load(URLRequest(url: url))
    }

    func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if let url = action.request.url,
           url.absoluteString.hasPrefix("https://remote.biometric.kz/finished") {
            decisionHandler(.cancel)
            dismiss(animated: true)
            fetchResult()
            return
        }
        decisionHandler(.allow)
    }

    func fetchResult() {
        // Request the result from your server using the session_id
    }
}
import android.webkit.WebView
import android.webkit.WebViewClient
import android.webkit.WebChromeClient
import android.webkit.PermissionRequest

class BiometricActivity : AppCompatActivity() {
    private lateinit var webView: WebView
    private val sessionId = "your_session_id"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_biometric)

        webView = findViewById(R.id.webView)
        webView.settings.apply {
            javaScriptEnabled = true
            mediaPlaybackRequiresUserGesture = false
            allowContentAccess = true
        }

        webView.webChromeClient = object : WebChromeClient() {
            override fun onPermissionRequest(request: PermissionRequest) {
                // Grant camera access
                request.grant(request.resources)
            }
        }

        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
                if (url?.startsWith("https://remote.biometric.kz/finished") == true) {
                    finish() // close the Activity
                    fetchResult()
                    return true
                }
                return false
            }
        }

        val url = "https://remote.biometric.kz/flow/$sessionId?web_view=true&locale=en"
        webView.loadUrl(url)
    }

    private fun fetchResult() {
        // Request the result from your server using the session_id
    }
}

Other platforms

If your application is built with a different framework (React Native, .NET MAUI, etc.), make sure the WebView configuration includes an equivalent of allowsInlineMediaPlayback to avoid the fullscreen player, and that camera permission requests are granted.

5. Handling completion#

When the verification is finished, the web application redirects to the following URL:

https://remote.biometric.kz/finished

You should intercept this navigation inside your application (see the navigation handlers in the examples above), close the WebView and continue your flow.

6. Getting the result#

After the WebView is closed, request the overall result of the session from your server:

Request URL:

https://kyc.biometric.kz/api/v1/flows/session/result/

Request format Request method
JSON GET
Parameter Type Obligatorily Description
session_id String Yes flow session identifier
flow_api_key String Yes API KEY of the created flow in the personal cabinet that was used when creating the session
curl --location --request GET 'https://kyc.biometric.kz/api/v1/flows/session/result/?session_id=<session_id>&flow_api_key=<flow_api_key>'

For the detailed description of the response fields, the LIGHT endpoint (temporary URLs instead of base64), liveness video download and PDF reports, see the Flow Remote documentation.

7. URL parameters#

The full list of parameters supported in the WebView URL:

Parameter Type Description
web_view boolean Mandatory. true — WebView mode
locale string Interface language: 'kz' | 'en' | 'ru' | 'my' | 'de' | 'es' | 'fa' | 'fr' | 'it' | 'ja' | 'kg' | 'ko' | 'pt'
isMobile boolean Force the device type (true/false)
documentType string Document type, e.g. passport (available values depend on the Flow configuration)
from_session_id string UUID of the previous session (session chaining)

Example URL with parameters:

https://remote.biometric.kz/flow/3fa85f64-5717-4562-b3fc-2c963f66afa6?web_view=true&locale=en&isMobile=true

8. Troubleshooting#

Problem Cause Solution
Camera does not start (iOS) allowsInlineMediaPlayback = false Set allowsInlineMediaPlayback = true
Camera does not start (Android) Permissions not granted Implement onPermissionRequest in WebChromeClient
Session not found (404) Wrong session_id Check the session_id
Session error 400 The session expired (30-minute TTL) or the API key belongs to another flow Create a new session; check the key matches the flow
White screen JavaScript disabled Enable javaScriptEnabled = true
Completion not caught The finished URL is not being tracked Add the check to the navigation delegate
INVALID session Fingerprint error or Too many sessions by fingerprint Create a new session; do not open one session_id in several WebViews