We use AES encryption to encrypt user identify of client and send it to eKYC Platform.
How it works:
A pair of:
Key: 256 bits (32 bytes)
IV (initialization vector): 16 bytes
TrustVision team will provide an (key, iv) pair and client_code for each customers before integration.
eg.
Pseudo code:
UserID = "user_id" // client-generated value
KeyBytes = Base64StdDecode(aesKey) // Base64 Standard Decoding
IVBytes = Base64StdDecode(aesIV) // Base64 Standard Decoding
// Step 1
EncryptedUserID = AES256CbcPkcs7PaddingEncrypt(UserID, KeyBytes, IVBytes)
// Step 2
EncodedBase64EncryptedValue = Base64UrlEncode(EncryptedUserID)
// Step 3
UniqueToken = KeyID + ":" + EncryptedUserID
// Step 4
EncodedBase64Token = Base64UrlEncode(UniqueToken) // Base64 URL Encoding
URL = "https://ekyc-platform-ph-staging.trustingsocial.com/lu/" + EncodedBase64
Sample code:
Reference:
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"github.com/zenazn/pkcs7pad"
)
func main() {
key := "YOUR_KEY"
iv := "YOUR_IV"
keyID := "YOUR_KEY_ID"
url := "YOUR_URL"
keyBytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
panic(err)
}
ivBytes, err := base64.StdEncoding.DecodeString(iv)
if err != nil {
panic(err)
}
block, err := aes.NewCipher(keyBytes)
if err != nil {
panic(err)
}
userID := "PLiOA_EdK3xx1M4sJPsOlQZqgX"
// Step 1: Padding userID and encrypt
contentPadding := pkcs7pad.Pad([]byte(userID), block.BlockSize())
encryptedValue := make([]byte, len(contentPadding))
blockMode := cipher.NewCBCEncrypter(block, ivBytes)
blockMode.CryptBlocks(encryptedValue, contentPadding)
// Step 2: Encode encrypted value to base64
encodedBase64EncryptedValue := base64.RawURLEncoding.EncodeToString(encryptedValue)
// Step 3: Generate unique token
uniqueToken := keyID + ":" + encodedBase64EncryptedValue
// Step 4: Encode unique token to base64
encodedBase64Token := base64.RawURLEncoding.EncodeToString([]byte(uniqueToken))
fmt.Println(url + "/" + encodedBase64Token)
}
username and password to receive JWT access token.| Method | Path | Content-Type | Request Body |
|---|---|---|---|
| POST | /auth/users/login | application/json | {"user_name":"user_name","password":"password"} |
| Name | Description | Status code | Response Body |
|---|---|---|---|
| Success | Login successfully | 200 | {"data":{"access_token":"<JWT>"},"message":"login success","time":"2024-10-31T07:34:52Z","verdict":"success"} |
| Bad Request | Invalid username pattern | 400 | {"data":{"invalid_parameters":["user_name"],"parameter_formats":{"user_name":"[regex: ^[a-z0-9_]{1,63}$]"}},"message":"some parameters has invalid format","time":"2024-11-06T03:30:42Z","verdict":"invalid_parameters"} |
| Unauthorized | Incorrent username or password | 401 | {"data":{},"message":"username or password is incorrect","time":"2024-10-31T07:50:22Z","verdict":"invalid_credential"} |
| Invalid Method | Invalid URL or method | 405 | {"data":{},"message":"method is not correct for the requested route","time":"2024-11-06T03:23:26Z","verdict":"invalid_method"} |
| Too many requests | Too many failed login attempts | 429 | {"data":{},"message":"too many failed login attempts","time":"2024-11-06T03:33:43Z","verdict":"limit_exceeded"} |
| Internal Server Error | Internal Server Error | 500 | {"data":{},"message":"Unexpected error. Error ID: ff4d5572d9df4560bb6dabc2eadd5a1e","time":"2024-11-06T03:32:09Z","verdict":"failure"} |
/application/detail| Method | Path | Content-Type | Request Body |
|---|---|---|---|
| GET | /portal/application/detail |
| Header key | Header Value | Description |
|---|---|---|
Authorization | Bearer <JWT> | Include the JWT access token to support request authentication |
| Parameter key | Description |
|---|---|
application_id | corressponding with the application_id in the callback result |
id | corressponding with the application_user_id in the callback result |
| Name | Description | Status code | Response Body |
|---|---|---|---|
| Success | request successfully | 200 | See the sample success response below |
| Bad Request | Invalid parameters | 400 | {"data":{},"message":"","verdict":"invalid_parameters"} |
| Unauthorized | Invalid authentication token | 401 | {"data":{},"message":"authorization header is missing","time":"2024-10-31T07:50:22Z","verdict":"missing_authorization"} |
| Token Expired | Token expired. Client should login again | 401 | {"data":{},"message":"Provided token is expired","time":"2024-10-31T07:50:22Z","verdict":"expired_token"} |
| Not found | Not found | 404 | {"data":{},"message":"applications not available","time":"2024-10-31T08:27:27Z","verdict":"record_not_found"} |
| Invalid Method | Invalid URL or method | 405 | {"data":{},"message":"method is not correct for the requested route","time":"2024-11-06T03:23:26Z","verdict":"invalid_method"} |
| Too many requests | Too many failed login attempts | 429 | {"data":{},"message":"too many failed login attempts","time":"2024-11-06T03:33:43Z","verdict":"limit_exceeded"} |
| Internal Server Error | Internal Server Error | 500 | {"data":{},"message":"Unexpected error. Error ID: ff4d5572d9df4560bb6dabc2eadd5a1e","time":"2024-11-06T03:32:09Z","verdict":"failure"} |
On success case: 200 OK
The response payload varies based on the flow configuration. Select a scenario below to see the relevant fields.
{
"data": {
"verdict": "pending|approve|review|reject",
"application_id": 10,
"application_unique_token": "ad083945-9620-486c-be7d-c3f5dab870a1",
"application_user_id": "01234567889",
"file_ids": {
"id_front_img_uuid": "8fdcb17e-5d41-4814-8d4f-8c1bfb51df20",
"id_back_img_uuid": "93cef1cd-2782-4264-9c65-cce78ce3f549",
"qr1_img_uuid": ""
},
"ekyc_ocr": {
"card_info": {
"card_info": [
{ "confidence_score": 0, "confidence_verdict": "", "field": "crn", "value": "0123456789" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "last_name", "value": "SANTOS" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "first_name", "value": "JOSE" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "middle_name", "value": "CRUZ" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "date_of_birth", "value": "1990/01/01" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "gender", "value": "F" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "address", "value": "CABAROAN SAN ESTEBAN ILOCOS SUR PHL 2706" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "id", "value": "0123456789" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "card_type", "value": "ph.ump" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "card_label", "value": "ph.ump.front" },
{ "confidence_score": 0, "confidence_verdict": "", "field": "name", "value": "JOSE CRUZ SANTOS" }
],
"converted_card_info": {
"address": "CABAROAN SAN ESTEBAN ILOCOS SUR PHL 2706",
"birth_date": "1990/01/01",
"card_label": "ph.ump.front",
"card_type": "ph.ump",
"gender": "F",
"id_number": "0123456789",
"name": "JOSE CRUZ SANTOS"
}
},
"process_status": "success",
"processed_at": "2024-09-26T18:11:29+07:00",
"sanity_check_result": "good",
"sanity_check_score": 1,
"sanity_check_status": "success",
"search_face_result": null,
"search_face_status": "disabled"
},
"id_tampering": {
"score": 1,
"verdict": "good"
},
"form_data": {
"contactInfo": {
"familyAddress": "CABAROAN SAN ESTEBAN ILOCOS SUR PHL 2706"
},
"personalInfo": {
"birthday": "1990/01/01",
"fullName": "JOSE CRUZ SANTOS",
"gender": "",
"idCard": "0123456789"
}
},
"time": "2024-09-27T09:50:32+07:00"
}
}
verdict response mapping:| verdict | Description |
|---|---|
| pending | The user has not completed the flow |
| approve | Our recommendation to approve this application |
| review | Our recommendation to review this application |
| reject | Our recommendation to reject this application |
file_ids response mapping:| key | Description | Present in |
|---|---|---|
| id_front_img_uuid | The image id of front side | ID Only, Full Flow |
| id_back_img_uuid | The image id of back side | ID Only, Full Flow |
| qr1_img_uuid | The image id of QR code | ID Only, Full Flow |
| selfie_image_uuid | The image id of selfie that use to compare faces | Selfie Only, Full Flow |
| gesture_images | List images during user do liveness | Selfie Only, Full Flow |
| sequence_frames | List frames video during user do liveness | Selfie Only, Full Flow |
| liveness_transformed_video_id | The video id merge from sequence frames | Selfie Only, Full Flow |
form_data response object:{
"contactInfo": {
"familyAddress": "CABAROAN SAN ESTEBAN ILOCOS SUR PHL 2706"
},
"personalInfo": {
"birthday": "1990/01/01",
"fullName": "JOSE CRUZ SANTOS",
"gender": "",
"idCard": "0123456789"
}
}
| Method | Path | Content-Type | Request Body |
|---|---|---|---|
| GET | /portal/document/download_ekyc_file |
| Header key | Header Value | Description |
|---|---|---|
Authorization | Bearer <JWT> | Include the JWT access token to support request authentication |
| Parameter key | Description |
|---|---|
application_id | corressponding with the application_id in the callback result |
id | corressponding with the file_ids in the callback result |
| Name | Description | Status code | Response Body |
|---|---|---|---|
| Success | request successfully | 200 | See the sample success response below |
| Bad Request | Invalid parameters | 400 | {"data":{},"message":"","verdict":"invalid_parameters"} |
| Unauthorized | Invalid authentication token | 401 | {"data":{},"message":"authorization header is missing","time":"2024-10-31T07:50:22Z","verdict":"missing_authorization"} |
| Token Expired | Token expired. Client should login again | 401 | {"data":{},"message":"Provided token is expired","time":"2024-10-31T07:50:22Z","verdict":"expired_token"} |
| Not found | Not found | 404 | {"data":{},"message":"applications not available","time":"2024-10-31T08:27:27Z","verdict":"record_not_found"} |
| Invalid Method | Invalid URL or method | 405 | {"data":{},"message":"method is not correct for the requested route","time":"2024-11-06T03:23:26Z","verdict":"invalid_method"} |
| Too many requests | Too many failed login attempts | 429 | {"data":{},"message":"too many failed login attempts","time":"2024-11-06T03:33:43Z","verdict":"limit_exceeded"} |
| Internal Server Error | Internal Server Error | 500 | {"data":{},"message":"Unexpected error. Error ID: ff4d5572d9df4560bb6dabc2eadd5a1e","time":"2024-11-06T03:32:09Z","verdict":"failure"} |
On success case: the HTTP status code will be 200, and server will send the file as binary content.
| Method | Path | Content-Type |
|---|---|---|
| POST | /portal/ekyc/index_faces | application/json |
| Header key | Header Value | Description |
|---|---|---|
Authorization | Bearer <JWT> | Include the JWT access token to support request authentication |
{
"image_base64": string,
"collection": string,
"label": string,
"metadata": JSON
}
| key | type | required | description | |
|---|---|---|---|---|
image_base64 | string | yes | encode base64 of binary image without prefix eg. data:image/jpeg;base64, | |
label | string | yes | label of the image as described at Label table | |
metadata | json | yes | more information of image. eg client_uid, phone_number, dob, name ... client_uid should be not empty | |
collection | string | no | index faces to this collection. case-sensitive |
Which each metadata's parameters contains:
| key | type | required | description |
|---|---|---|---|
client_uid | string | yes | ID of an user or application or any unique token |
national_id | string | no | id number of ID Card of an user |
unique_token | string | no | the token application_unique_token from application detail |
| Name | Description | Status code | Response Body |
|---|---|---|---|
| Success | request successfully | 200 | See the sample success response below |
| Bad Request | Invalid parameters | 400 | {"data":{},"message":"","verdict":"invalid_parameters"} |
| Unauthorized | Invalid authentication token | 401 | {"data":{},"message":"authorization header is missing","time":"2024-10-31T07:50:22Z","verdict":"missing_authorization"} |
| Token Expired | Token expired. Client should login again | 401 | {"data":{},"message":"Provided token is expired","time":"2024-10-31T07:50:22Z","verdict":"expired_token"} |
| Not found | Not found | 404 | {"data":{},"message":"applications not available","time":"2024-10-31T08:27:27Z","verdict":"record_not_found"} |
| Invalid Method | Invalid URL or method | 405 | {"data":{},"message":"method is not correct for the requested route","time":"2024-11-06T03:23:26Z","verdict":"invalid_method"} |
| Too many requests | Too many failed login attempts | 429 | {"data":{},"message":"too many failed login attempts","time":"2024-11-06T03:33:43Z","verdict":"limit_exceeded"} |
| Internal Server Error | Internal Server Error | 500 | {"data":{},"message":"Unexpected error. Error ID: ff4d5572d9df4560bb6dabc2eadd5a1e","time":"2024-11-06T03:32:09Z","verdict":"failure"} |
On success case: the HTTP status code will be 200, and server will send the file as binary content.
| Method | Path | Content-Type |
|---|---|---|
| POST | /portal/ekyc/full_checks | application/json |
| Header key | Header Value | Description |
|---|---|---|
Authorization | Bearer <JWT> | Include the JWT access token to support request authentication |
{
"card_type": string,
"image1": {
"base64": string,
"label": string,
"metadata": JSON
},
"image2": {
"base64": string,
"label": string,
"metadata": JSON
},
"selfies": [
{
"base64": string,
"label": string,
"metadata": JSON
},
...
{
"base64": string,
"label": string,
"metadata": JSON
}
]
"metadata": JSON
}
| key | type | required | description |
|---|---|---|---|
| `card_type | string | yes | card type of the image as described at Card types table |
image1 | ImageData | yes | image of the identity card's front side |
image2 | ImageData | no | image of the identity card's back side |
selfies | []ImageData | no | list selfie images of customer. Max 3 images |
metadata | json | yes | more information of identity. eg client_uid, phone_number, dob, name ... client_uid should be not empty |
Which each image's parameters contains:
| key | type | required | description |
|---|---|---|---|
base64 | string | yes | base64 encoded text of image data |
label | string | no | label of the image as described at Label table |
metadata | json | no | key-value, key should be string, value should be string, int, float, bool. Example "{\"id\":\"123456789\",\"type\":1}" |
Which each metadata's parameters contains:
| key | type | required | description |
|---|---|---|---|
client_uid | string | yes | ID of an user or application or any unique token |
national_id | string | no | id number of ID Card of an user |
unique_token | string | no | the token application_unique_token from application detail |
| Name | Description | Status code | Response Body |
|---|---|---|---|
| Success | request successfully | 200 | See the sample success response below |
| Bad Request | Invalid parameters | 400 | {"data":{},"message":"","verdict":"invalid_parameters"} |
| Unauthorized | Invalid authentication token | 401 | {"data":{},"message":"authorization header is missing","time":"2024-10-31T07:50:22Z","verdict":"missing_authorization"} |
| Token Expired | Token expired. Client should login again | 401 | {"data":{},"message":"Provided token is expired","time":"2024-10-31T07:50:22Z","verdict":"expired_token"} |
| Invalid Method | Invalid URL or method | 405 | {"data":{},"message":"method is not correct for the requested route","time":"2024-11-06T03:23:26Z","verdict":"invalid_method"} |
| Too many requests | Too many failed login attempts | 429 | {"data":{},"message":"too many failed login attempts","time":"2024-11-06T03:33:43Z","verdict":"limit_exceeded"} |
| Internal Server Error | Internal Server Error | 500 | {"data":{},"message":"Unexpected error. Error ID: ff4d5572d9df4560bb6dabc2eadd5a1e","time":"2024-11-06T03:32:09Z","verdict":"failure"} |
On success case: 200 OK
{
"data": {
"request_id": "538cdfe9-7009-4bcf-b514-6faf8904e82c",
"card_information": [
{
"field": "id",
"value": "123456789012",
"confidence_verdict": "",
"confidence_score": 0
},
{
"field": "name",
"value": "FULL NAME",
"confidence_verdict": "",
"confidence_score": 0
},
{
"field": "date_of_birth",
"value": "2005/03/02",
"confidence_verdict": "",
"confidence_score": 0
},
{
"field": "gender",
"value": "F",
"confidence_verdict": "",
"confidence_score": 0
},
{
"field": "address",
"value": "RD 1000 NDC COMPOUND,, SITH DISTRICT",
"confidence_verdict": "",
"confidence_score": 0
},
{
"field": "last_name",
"value": "LAST NAME",
"confidence_verdict": "",
"confidence_score": 0
},
{
"field": "first_name",
"value": "FIRST NAME",
"confidence_verdict": "",
"confidence_score": 0
},
{
"field": "middle_name",
"value": "MIDDLE NAME",
"confidence_verdict": "",
"confidence_score": 0
},
{
"field": "card_type",
"value": "ph.philhealth",
"confidence_verdict": "",
"confidence_score": 0
},
{
"field": "card_label",
"value": "ph.philhealth.qr.front",
"confidence_verdict": "",
"confidence_score": 0
}
],
"compare_faces": [
{
"face1_id": "7d71e6e2-a6bb-4ea6-8826-be0947afd6f4",
"face2_id": "1647fae2-7ff2-4500-89cc-4196e42512ce",
"result": "matched",
"score": 0.8136464
}
],
"liveness_check": {
"is_live": true,
"score": 0.99999917
},
"portrait_sanity": {
"verdict": "good",
"score": 1
},
"card_sanity": {
"verdict": "photo_not_qualified",
"score": 0.8199463,
"score_details": [
{
"verdict": "good",
"score": 1
},
{
"verdict": "photo_not_qualified",
"score": 0.8199463
}
],
"verdict_details": {}
},
"card_tampering": {
"verdict": "alert",
"score": 1,
"details": [
{
"verdict": "non_liveness",
"score": 0.99,
"name": "alert_1",
"info": "image1"
}
]
}
},
"message": "fully check eKYC successfully",
"time": "2026-01-27T18:03:56+07:00",
"verdict": "success"
}
verdict response mapping:| verdict | Description |
|---|---|
| success | Process the request successfully. |
| failure | Process the request failure. |
Uploads a single image file to eKYC and returns the image_id that can be referenced from API Vision Score's images[].id (and other batch endpoints) instead of re-sending the bytes.
| Method | Path | Content-Type |
|---|---|---|
| POST | /portal/ekyc/upload/image | multipart/form-data |
| Header key | Header Value | Description |
|---|---|---|
Authorization | Bearer <JWT> | Include the JWT access token to support request authentication |
| Field | Type | Required | Description |
|---|---|---|---|
file | file | yes | Image file. Must be non-empty. Maximum size is governed by the server's tv_max_file_size setting. |
label | string | no | Image label as described at Label table. |
metadata | string | no | JSON-encoded string with per-image metadata. |
{
"data": {
"image_id": "8fdcb17e-5d41-4814-8d4f-8c1bfb51df20"
},
"message": "image uploaded",
"time": "2026-05-04T09:00:00+07:00",
"verdict": "success"
}
Synchronously runs Vision Score (face liveness + selfie quality) for a set of selfie images. Each image may be supplied inline as base64 or by id (returned from API Upload Image).
| Method | Path | Content-Type |
|---|---|---|
| POST | /portal/ekyc/vision_score | application/json |
| Header key | Header Value | Description |
|---|---|---|
Authorization | Bearer <JWT> | Include the JWT access token to support request authentication |
{
"images": [
{
"id": "8fdcb17e-5d41-4814-8d4f-8c1bfb51df20"
},
{
"base64": "<base64_string>",
"metadata": "{\"frame_index\":1}"
}
],
"gesture_images": [
{
"id": "fa9a4fee-41c3-4988-a126-1431c130aa94"
}
],
"videos": [
{
"id": "b5a23e55-773f-4cf2-985b-835b9b3d0077"
}
],
"metadata": {
"applicant_name": "e3b0c44298fc1c149afb...a1b2c3",
"id_number": "a665a45920422f9d417e...f4c8b1",
"dob": "9f86d081884c7d659a2f...3d5c2a",
"gender": "male",
"phone_number": "b14a7b8059d9c055954c...8e7f12",
"email": "2cf24dba5fb0a30e26e8...9d1c4a",
"application_time": "2025-05-02T10:30:00+07:00",
"loan_amount": 5000000,
"loan_currency": "VND",
"loan_product_type": "personal_loan",
"acquisition_channel": "agent",
"is_existing_customer": false,
"device_type": "android",
"device_model": "Samsung Galaxy A54"
},
"selfie_type": "passive",
"flow_id": "vision_score_only",
"lender_id": 0
}
| key | type | required | description |
|---|---|---|---|
images | []ImageData | yes | Selfie images. The last element is treated as the primary selfie. |
gesture_images | []ImageData | no | Gesture (active liveness) images. |
videos | []ImageData | no | Video frames captured during the liveness flow. |
metadata | json | no | Contextual data about the loan application and applicant. See Metadata Reference. |
selfie_type | string | no | Selfie liveness type. Common values: passive, flash, active. |
flow_id | string | no | Routes the request to the lender config mapped to this flow via the encryption key's flow_lender_config_mapping. Defaults to vision_score_only. |
lender_id | int | no | Admin-only override that scopes the call to a specific tenant. Non-admin callers always resolve the lender from their JWT and any supplied value is ignored. |
Where each ImageData object accepts either id (preferred — references a pre-uploaded image) or base64 (inline upload):
| key | type | required | description |
|---|---|---|---|
id | string | one of | image_id returned by API Upload Image. Use this to avoid re-sending large payloads. |
base64 | string | one of | Base64-encoded image content without the data:image/jpeg;base64, prefix. Required when id is not supplied. |
metadata | string | no | Per-image metadata string forwarded to eKYC. Example: "{\"frame_index\":0}". |
| Mode | images | gesture_images | videos | selfie_type | flow_id |
|---|---|---|---|---|---|
| Vision Score only | 1 image | — | — | — | vision_score_only |
| Vision Score with Passive Liveness | 1–3 images | — | optional | passive | vision_score_sanity_liveness |
The metadata field accepts a dictionary of contextual data associated with the loan application and applicant. Providing this information enables TS to improve model performance over time and unlocks eligibility for model fine-tuning. All fields are optional. Keys outside the list below are accepted and stored but carry no guaranteed semantic interpretation.
| Key | Type | Encrypt | Description |
|---|---|---|---|
applicant_name | string | Yes | Full name as captured from ID document |
id_number | string | Yes | National ID or passport number |
dob | string | Yes | Date of birth, ISO 8601 format (e.g. "1990-03-15") |
gender | string | No | Accepted values: "male", "female", "other" |
phone_number | string | Yes | Registered mobile number in E.164 format (e.g. "+84901234567") |
email | string | Yes | Applicant email address |
| Key | Type | Encrypt | Description |
|---|---|---|---|
application_time | string | No | ISO 8601 timestamp of when the loan application was submitted (e.g. "2025-05-02T10:30:00+07:00") |
loan_amount | float | No | Requested loan amount in local currency |
loan_currency | string | No | ISO 4217 currency code (e.g. "VND", "PHP", "USD") |
loan_product_type | string | No | Product category (e.g. "personal_loan", "bnpl", "salary_advance", "sme_loan") |
acquisition_channel | string | No | Channel through which the applicant was acquired (e.g. "organic", "agent", "referral", "digital_ads") |
is_existing_customer | bool | No | Whether the applicant is an existing borrower |
| Key | Type | Encrypt | Description |
|---|---|---|---|
device_type | string | No | Accepted values: "android", "ios", "web" |
device_model | string | No | Device model name (e.g. "Samsung Galaxy A54") |
PII fields below are shown as SHA-256 hashes. Plain-text values must never be transmitted.
{
"metadata": {
"applicant_name": "e3b0c44298fc1c149afb...a1b2c3",
"id_number": "a665a45920422f9d417e...f4c8b1",
"dob": "9f86d081884c7d659a2f...3d5c2a",
"gender": "male",
"phone_number": "b14a7b8059d9c055954c...8e7f12",
"email": "2cf24dba5fb0a30e26e8...9d1c4a",
"application_time": "2025-05-02T10:30:00+07:00",
"loan_amount": 5000000,
"loan_currency": "VND",
"loan_product_type": "personal_loan",
"acquisition_channel": "agent",
"is_existing_customer": false,
"device_type": "android",
"device_model": "Samsung Galaxy A54"
}
}
| Name | Description | Status code | Response Body |
|---|---|---|---|
| Success | request successfully | 200 | See the sample success response below |
| Bad Request | Invalid parameters / missing config | 400 | {"data":{},"message":"eKYC platform URL not configured","verdict":"failure"} |
| Unauthorized | Invalid authentication token | 401 | {"data":{},"message":"authorization header is missing","time":"2024-10-31T07:50:22Z","verdict":"missing_authorization"} |
| Token Expired | Token expired. Client should login again | 401 | {"data":{},"message":"Provided token is expired","time":"2024-10-31T07:50:22Z","verdict":"expired_token"} |
| Bad Gateway | Upstream Platform Init / Verify failed | 502 | {"data":{},"message":"platform init failed: ...","verdict":"failure"} / {"data":{},"message":"platform verify selfie failed: ...","verdict":"failure"} |
| Internal Server Error | Cannot create encrypted token | 500 | {"data":{},"message":"cannot create encrypted token","verdict":"failure"} |
On success case: 200 OK
The success response follows this structure:
{
"data": {
"unique_token": string, // eKYC application unique token (issued by Platform Init)
"verify_response": { // upstream Platform Verify Face Liveness result
"status": string, // "success" | "failure"
"application_state": {
"current_step": string, // current step in the eKYC flow, e.g. "success"
"fully_completed": bool, // true when every required state is completed
"required_states": [string], // states the flow requires the user to complete
"all_completed_states": [string], // states the user has completed so far
"client_custom_settings": {
"allow_card_types": [string], // card types permitted for this flow
"redirect_url": string // post-flow redirect URL, if configured
}
}
},
"application": { // optional — present only when the application is found
"verdict": string, // "pending" | "approve" | "review" | "reject"
"application_id": int,
"application_unique_token": string,
"application_user_id": string,
"ekyc_face": {
"vision_score": number, // range 0–100; higher = more trustworthy / lower risk
"vision_score_status": string, // "success" | "failure"
"vision_score_request_id": string, // request id for tracing Vision Score logs
"liveness_score": number, // passive-liveness score, range 0–1
"liveness_status": string, // "success" | "failure"
"sanity_check_score": number, // selfie quality score
"sanity_check_result": string, // selfie quality verdict, e.g. "good"
"sanity_check_status": string, // "success" | "failure"
"search_face_result": object, // face-search hits; null when disabled
"search_face_status": string, // e.g. "found"; "" when disabled
"process_status": string, // "success" | "failure"
"processed_at": string, // ISO 8601 timestamp
"selfie_image_uuid": string, // image id of the primary selfie
"gesture_images": [ { "gesture": string, "image_id": string } ],
"sequence_frames": [ ... ], // liveness video frames
"liveness_transformed_video_id": string // video merged from sequence frames
},
"file_ids": { // image/video ids captured during the flow
"selfie_image_uuid": string,
"gesture_images": [ { "gesture": string, "image_id": string } ],
"sequence_frames": [ ... ],
"liveness_transformed_video_id": string
},
"time": string // ISO 8601 timestamp
}
},
"message": string, // human-readable status, e.g. "vision score check completed"
"time": string, // ISO 8601 timestamp of the response
"verdict": string // "success" on HTTP 200; see the error table above for other values
}
The scoring outputs of this endpoint live under application.ekyc_face. vision_score is the primary output; the liveness and sanity-check fields describe the quality of the captured selfie.
vision_score (0–100). A higher score means a more trustworthy / lower-risk profile. It is computed independently of the liveness result — a failed liveness check does not necessarily lower the Vision Score.| Group | Field | Type | Description |
|---|---|---|---|
| Vision Score | vision_score | number | Vision Score, range 0–100. Higher = more trustworthy / lower risk. |
| Vision Score | vision_score_status | string | "success" when the score was computed, otherwise "failure". |
| Vision Score | vision_score_request_id | string | Request id used to trace Vision Score processing in logs. |
| Liveness | liveness_score | number | Passive-liveness score, range 0–1. |
| Liveness | liveness_status | string | "success" or "failure". |
| Sanity check | sanity_check_score | number | Selfie quality score. |
| Sanity check | sanity_check_result | string | Selfie quality verdict, e.g. "good". |
| Sanity check | sanity_check_status | string | "success" or "failure". |
{
"data": {
"application": {
"application_id": 100010221,
"application_unique_token": "7ae4e342-6cec-4a4e-bd6e-831784391d40",
"application_user_id": "5b506b16-7928-44ea-b690-1ef61d39ed08",
"ekyc_face": {
"gesture_images": [
{
"gesture": "frontal",
"image_id": "186c1ca9-cc99-4fc7-8742-036a2ea8e244"
}
],
"liveness_score": 0.99999034,
"liveness_status": "success",
"liveness_transformed_video_id": "",
"process_status": "success",
"processed_at": "2026-06-17T03:17:22Z",
"sanity_check_result": "good",
"sanity_check_score": 1,
"sanity_check_status": "success",
"search_face_result": null,
"search_face_status": "",
"selfie_image_uuid": "186c1ca9-cc99-4fc7-8742-036a2ea8e244",
"sequence_frames": [],
"vision_score": 86,
"vision_score_request_id": "eec55581-33a0-4d3a-8247-81202609f75b",
"vision_score_status": "success"
},
"file_ids": {
"gesture_images": [
{
"gesture": "frontal",
"image_id": "186c1ca9-cc99-4fc7-8742-036a2ea8e244"
}
],
"liveness_transformed_video_id": "",
"selfie_image_uuid": "186c1ca9-cc99-4fc7-8742-036a2ea8e244",
"sequence_frames": []
},
"time": "2026-06-17T03:17:22Z",
"verdict": "approve"
},
"unique_token": "7ae4e342-6cec-4a4e-bd6e-831784391d40",
"verify_response": {
"application_state": {
"all_completed_states": ["ekyc.selfie.passive_v2"],
"client_custom_settings": {
"allow_card_types": ["vn.national_id"],
"redirect_url": ""
},
"current_step": "success",
"fully_completed": true,
"required_states": ["ekyc.selfie.passive_v2"]
},
"status": "success"
}
},
"message": "vision score check completed",
"time": "2026-06-17T03:17:22Z",
"verdict": "success"
}
| key | type | description |
|---|---|---|
unique_token | string | The eKYC application unique token issued by Platform Init. Use it to look up the application via API Application Detail. |
verify_response | object | The data block returned by the upstream Platform Verify Face Liveness API — same shape as the Selfie Verification API response. |
application | object | Optional — only present when an application matching unique_token is found for the lender. Same shape as the Application Detail response. |
application.verdict follows the same mapping as API Application Detail (pending | approve | review | reject).
The scoring outputs under application.ekyc_face are summarized in Key Result Fields above.
The remaining ekyc_face and file_ids fields (compare_score, search_face_result, gesture_images, sequence_frames, liveness_transformed_video_id, …) follow the same shape as the API Application Detail response.
verify_response.application_state reports the flow's progress:
| key | type | description |
|---|---|---|
current_step | string | Current step in the eKYC flow, e.g. "success". |
fully_completed | bool | true when every required state has been completed. |
required_states | []string | States the flow requires the user to complete. |
all_completed_states | []string | States the user has completed so far. |
client_custom_settings | object | Per-client flow settings, e.g. allow_card_types, redirect_url. |
The Platform API allows clients to drive the eKYC journey programmatically via direct HTTP calls, without using WebView or mobile SDKs.
Base endpoint:
access_token.access_token as a Bearer token in subsequent calls:Authorization: Bearer <access_token>
Initializes an eKYC session from the client-generated encrypted token. This single call handles token parsing, application creation (or lookup), bootstrap configuration, and access token issuance.
| Method | Path | Content-Type | Auth Required |
|---|---|---|---|
| GET | /ekyc/platform/init | — | No |
Query Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| encrypted_token | string | Yes | The ENCODED_VALUE generated by the Authentication mechanism above (AES-256 CBC encrypted user identity, Base64 URL encoded with key ID prefix) |
| flow_id | string | No | Specific flow to initialize |
| card_types | string | No | Comma-separated list of allowed card types (e.g. vn.national_id,vn.passport) |
| lang | string | No | Language code (e.g. vi, en) |
Success (200):
{
"data": {
"token": "<unique_token>",
"access_token": "<JWT>",
"bootstrap": {
"lender_code": "lender_vn",
"lender_country": "VN",
"lender_language": "vi",
"ui_version": "4.0",
"masked_phone_number": "09*****123",
"required_steps": [],
"flow_selected_at": 1700000000,
"lead_source": "sms",
"client_custom_settings": {}
}
},
"message": "platform init successfully",
"time": "2026-01-01T00:00:00Z",
"verdict": "success"
}
| verdict | Status code | Description |
|---|---|---|
| success | 200 | Session initialized successfully. |
| invalid_parameters | 400 | encrypted_token is missing or has invalid format. |
| record_not_found | 404 | Encryption key, lender, or application not found / expired. |
| limit_exceeded | 429 | Concurrent init request in progress — retry shortly. |
| failure | 500 | Internal server error. |
Uploads ID card images (and optional QR images), runs OCR and tampering checks, and returns the final verification result synchronously.
| Method | Path | Content-Type | Auth Required |
|---|---|---|---|
| POST | /ekyc/platform/verify_id | application/json | Yes |
Request Body:
| Name | Type | Required | Description |
|---|---|---|---|
| card_type | string | Yes | Card type to verify (e.g. vn.national_id, vn.passport, ph.philhealth) |
| image | object | Yes | Front side image: { "base64": "<base64_string>", "metadata": "<string>", "qr_code": [{"result": "...", "from_image_type": "..."}] } |
| image2 | object | No | Back side image: same structure as image |
| qrs | array | No | QR code images: [{ "base64": "<base64_string>", "metadata": "<string>" }] |
Success (200):
{
"data": {
"status": "success",
"error_code": "",
"error_message": {},
"label": "vn.national_id.front",
"next_action": "",
"application_state": ""
},
"message": "verify id card completed",
"time": "2026-01-01T00:00:00Z",
"verdict": "success"
}
Response data fields:
| Field | Type | Description |
|---|---|---|
| status | string | Verification result: success, failure, partial_success, etc. |
| error_code | string | Error code when status is failure (omitted on success) |
| error_message | object | Human-readable error details (omitted on success) |
| label | string | Image label that was processed (e.g. vn.national_id.front) |
| next_action | string | Action the client should take next (e.g. process_qr_code) — if applicable |
| application_state | string | Updated application state after verification |
| verdict | Status code | Description |
|---|---|---|
| success | 200 | Verification completed (check data.status for result). |
| invalid_parameters | 400 | Missing or invalid card_type, image, or base64 data. |
| limit_exceeded | 429 | A verification is already in progress for this session. |
| failure | 500 | Internal server error or verification timeout. |
Uploads selfie images, runs liveness detection and face matching, and returns the final verification result synchronously. The last image in the images array is treated as the primary selfie.
| Method | Path | Content-Type | Auth Required |
|---|---|---|---|
| POST | /ekyc/platform/verify_selfie | application/json | Yes |
Request Body:
| Name | Type | Required | Description |
|---|---|---|---|
| images | array | Yes | Array of selfie images: [{ "base64": "<base64_string>", "metadata": "<string>" }]. The last element is the primary selfie. |
| selfie_type | string | No | Type of selfie flow (e.g. passive, flash) |
Success (200):
{
"data": {
"status": "success",
"error_code": "",
"error_message": {},
"label": "portrait",
"next_action": "",
"application_state": ""
},
"message": "verify selfie completed",
"time": "2026-01-01T00:00:00Z",
"verdict": "success"
}
Response data fields are identical to Platform Verify ID above.
| verdict | Status code | Description |
|---|---|---|
| success | 200 | Verification completed (check data.status for result). |
| invalid_parameters | 400 | images is empty or contains invalid base64 data. |
| limit_exceeded | 429 | A verification is already in progress for this session. |
| failure | 500 | Internal server error or verification timeout. |
Bundles the three calls above (Init → ID Verification → Selfie Verification) into a single synchronous request. It imports a complete dossier, runs the configured ID and selfie checks in sequence (waiting for each), drives the application to a terminal state so the configured Post-check / AML / Government checks run, and returns the full TS-native result — the same shape as API Application Detail — in the response.
Use this when you already hold a user's ID/selfie assets (e.g. captured on another channel or by another vendor) and want to re-check them through the TrustVision pipeline in one call, without driving Init / Verify ID / Verify Selfie separately.
Authentication: unlike Verify ID / Verify Selfie, this endpoint does not require a prior Init or a Bearer
access_token— it authenticates with theencrypted_tokenin the body (the same token as Init). The response still returns anaccess_tokenyou can use to re-pull API Application Detail later.
Note: the request blocks until all checks finish (ID + selfie run sequentially), so allow a generous client timeout.
| Method | Path | Content-Type | Auth Required |
|---|---|---|---|
| POST | /ekyc/platform/aggregate | application/json | No |
Request Body:
| Name | Type | Required | Description |
|---|---|---|---|
| encrypted_token | string | Yes | The ENCODED_VALUE from the Authentication mechanism (same as Init). Identifies the user; the user reference inside it is the idempotency key. |
| flow_id | string | No | Specific flow to use. |
| card_types | string | No | Comma-separated allowed card types (e.g. vn.national_id,vn.passport). |
| lang | string | No | Language code (e.g. vi, en). |
| card_type | string | Yes (for ID) | Card type to verify (e.g. vn.national_id). Must be within card_types when that is provided. |
| image1 | object | Yes (for ID) | ID front: { "base64": "<base64>", "metadata": "<string>", "qr_code": [{"result": "...", "from_image_type": "..."}] }. |
| image2 | object | No | ID back: same structure as image1. |
| qrs | array | No | Standalone QR images: [{ "base64": "<base64>", "metadata": "<string>" }]. |
| selfies | array | Yes (for selfie) | Frontal selfie image(s): [{ "base64" \| "id", "metadata" }]. The last element is the primary selfie used for face compare. An element with id references a pre-uploaded image. |
| selfie_type | string | No | Selfie / liveness type (e.g. passive, passive_v2, flash). |
| videos | array | No | Liveness video frames: [{ "id"?, "metadata"?, "frames": [{ "base64", "label", "index", "metadata"? }] }] (≤ 10). Required for passive/flash liveness to pass — a still-only selfie gives the engine nothing to score. |
| application_metadata | object | No | Prepared applicant identity PII (applicant_name, id_number, dob, gender, phone_number, email), JSON ≤ 8 KB. Stored encrypted and echoed back only — never fed into any check or the verdict. |
At least one of image1 (ID) or selfies (selfie) must be present — supply only one group for an ID-only or selfie-only re-check. Put liveness frames in videos, not in selfies.
The endpoint blocks until all checks complete, then returns the full result — the same TS-native shape as API Application Detail (overall verdict plus each module's status). Conditional blocks (ekyc_ocr / ekyc_face / id_tampering / file_ids / form_data / application_metadata) appear only when the corresponding data exists.
Success (200):
{
"data": {
"verdict": "approve",
"application_id": 10,
"application_unique_token": "ad083945-...",
"application_user_id": "0123456789",
"ekyc_ocr": {
"card_info": {},
"process_status": "success",
"sanity_check_status": "success"
},
"ekyc_face": {
"compare_status": "matched",
"compare_score": 0.98,
"liveness_status": "live",
"search_face_status": "not_found"
},
"id_tampering": { "score": 0.01, "verdict": "genuine" },
"application_metadata": {
"applicant_name": "Jane Doe",
"gender": "female"
},
"access_token": "<JWT>"
},
"message": "aggregate completed",
"time": "2026-01-01T00:00:00Z",
"verdict": "success"
}
Response data fields:
| Field | Description |
|---|---|
| data.verdict | Overall application verdict: approve, review, reject, or pending. |
| ekyc_ocr / ekyc_face / id_tampering | Per-module results + status (same as API Application Detail). Present only when run. |
| application_metadata | The prepared PII you supplied, decrypted and echoed back (omitted if none). |
| access_token | JWT to re-pull API Application Detail for this application later. |
See API Application Detail above for the full field reference (the verdict, file_ids, and form_data mappings).
| verdict | Status code | Description |
|---|---|---|
| success | 200 | Processing completed — read data.verdict for the outcome. |
| missing_parameters | 400 | encrypted_token is missing. |
| invalid_parameters | 400 | Token undecodable, no ID/selfie assets, bad card_type, or invalid base64. |
| record_not_found | 404 | Encryption key or lender not found / inactive. |
| limit_exceeded | 429 | Another submission for the same user is in progress, or rate limit reached. |
| failure | 500 | Internal server error. |
Dedupe / indexing-only mode: when enabled for the lender (client_custom_settings.aggregate_dedupe_only), the endpoint runs only face index/search, leaves data.verdict as pending, and returns the match result on data.ekyc_face.search_face_result / search_face_status (the Post-check / AML / verdict pipeline is skipped).