eKYC Platform Integration Guidelines

Prerequisites

Our client will be provided following credentials to initialize a unique token for an user flow:

I. Generate token to send to eKYC Platform

Reference Authentication mechanism

II. Integrate with your platform

Two integration modes are available. Choose based on your use case, then follow the guide for your platform.

Pre-built full eKYC journeysFlexible ID & Selfie Capturing

Full journey managed by TrustVision. Low-code.

Example flow_id values:

id_selfie

selfie_id

id_nfc_selfie

qr_nfc_selfie

id_form_selfie

vision_score_sanity_liveness

vision_score_only

… and more

Built-in. No provisioning needed.

💡 Flexible flows — Contact TrustVision to receive your custom flow_id before integrating. Once you have it, use it exactly the same way as the built-in flow_id values shown below.

II.1 Web or WebView

Use EncodedBase64Token generated from step I and concatenate to provided URL

Final URL:

Production
The production host follows the same naming pattern, with `-production` in place of `-staging` — for example, `https://ekyc-platform-ph-production.trustingsocial.com/lu/[EncodedBase64Token]`.

Optional query parameters

You can append the following optional query parameters to the URL to control which eKYC journey runs, the allowed ID card types, and the UI language. Every parameter is optional — omit one to fall back to the default configured for your token.

ParameterTypeRequiredDescription
flow_idstringNoSelects which eKYC journey to run. Built-in flows include id_selfie (ID → Selfie → Matching) and selfie_id (Selfie → ID → Matching). Flexible/custom flows — such as ID-only, Selfie-only, or ID → Form → Selfie — are also supported. The exact flow_id for each flow is provisioned per client by TrustVision. If omitted, the default flow configured for your token is used.
card_typesstringNoComma-separated list of allowed ID card types, e.g. vn.national_id,vn.passport. Restricts which ID documents the user can submit. If omitted, all card types configured for your account are allowed.
languagestringNoUI language for the eKYC journey. One of vi, en, or es-mx. If omitted, the lender's default language is used.

Note: The language query parameter corresponds to the lang parameter of the Platform Initialization API.

Examples

Built-in flow (ID → Selfie) with Vietnamese UI:

https://ekyc-platform-vn-staging.trustingsocial.com/lu/[EncodedBase64Token]?flow_id=id_selfie&language=vi

Flexible flow (ID only), restricted to the national ID card, with English UI:

https://ekyc-platform-vn-staging.trustingsocial.com/lu/[EncodedBase64Token]?flow_id=[YOUR_ID_ONLY_FLOW_ID]&card_types=vn.national_id&language=en

Replace [YOUR_ID_ONLY_FLOW_ID] with the flow ID provisioned for you by TrustVision.

II.2 Android

II.2.1 eKYC Platform

Refer to this for more details.

II.2.2 eKYC Platform Webview

Refer to this for more details.

II.3 iOS

II.3.1 eKYC Platform

Refer to this for more details.

II.3.2 eKYC Platform Webview

Refer to this for more details.

II.4 Flutter

II.4.1 eKYC Platform

Refer to this for more details.

II.4.2 eKYC Platform Webview

Refer to this for more details.

II.5 React Native

II.5.1 eKYC Platform

Refer to this for more details.

II.5.2 eKYC Platform Webview

Refer to this for more details.

III. How client will get the result

III.1 Use callback URL

  • Use-case: TrustingSocial's Backend call to Client's Backend.
  • The result will be sent to the client's callback URL in JSON format.
  • The client should verify the signature that was signed using the TS RSA key.

Sample result:

View result by flow type: Full Flow | ID Only

{
  "verdict": "pending|approve|review|reject",
  "signature": "ImFwcGxpY2F0aW9uX3VuaXF1ZV90b2tlbvN4A6zZUr3SAg7mH6vUiypRRhbCsyoVU9E2/ZFQywpPTcQ2tUC3i66jy85yjwL4XDiaEgQBJh9ZzOQAl8vKbo2cH/rVkOJYQR8HLOE5yeBhHFk9xKVfMyywtXwpaHkMH+U7AWUqPvyYnRMfHC3Pgx/V3npDvDrRSMpRLe9pAKw+1GSrGnw+wY7eTHVWeo6ilQnJ3r8nSI4yZGxnoUpffQenW8tPYuRYJuODUiXochsm408oSIzyZCvyPWkTCso93P2xnEshgMbHkqyTGUeOoePdJT9Z3OolbZbkqyEbQ==",
  "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"
}

Explanation of the fields:

Field nameDatatypeDescription
application_idintApplication ID returned by TV, can be used to get application detail later
application_unique_tokenstringthe unique token for User Session, generated after decrypting the client's encrypted token
application_user_idstringthe decrypted client-generated user ID
file_idsobjectContains images, frame video, merged video.
ekyc_faceobjectFace image processing results including: sanity check, liveness check, comparison (with ID card face), searching
ekyc_ocrobjectID Card image processing result including: sanity check, card info, search faces
form_dataobjectThe user-filled form
id_tamperingobjectID card tampering check
verdictstringReference
timestringThe time of the message
All the score fieldsfloat/decimalDecimal number between 0 and 1 inclusively

How to verify signature from JSON data:

- Public key:

Philippines environment:

JSON
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwC4C2c+nbI5GN3RseYUE
6xB1SObAk1HNFrs41sbBL1ZgMcYmXnuPrqXehzdqbBSYnWwFufLwFnFfAox7dInw
2olB8QCX9aOYXxcCX+T4JKJxYMuMC7+vuKVip22U+6WUTOaHqGAB2ZmsGPp7dpN4
Q+5duwiYam4izHwiFZ2bGmXbDqARtRlO+UnA8JMVE9LISQD1p238Id+AY3K9W8GQ
W3luLJttKZ0/ulJ54JsVxg5wF3EtohfcvVSJD/6rY0BLUINB5o9nLuGYkjpd+7V0
oB+o4YDMPMOTu8IJa0tbZg8HClr+/Rq3W4xle/NFS1YbLmp45MJ8APsbX9OkG+jM
JwIDAQAB
-----END PUBLIC KEY-----

- To verify the received data, follow these steps:

  1. Extract the signature: Isolate the value of the signature field from the received JSON data.
  2. Prepare the data payload: Create a representation of the JSON data excluding the signature field. This is the original data that was signed.
  3. Verify the signature: Using the public key, verify the extracted signature (from step 1) against the prepared data payload (from step 2).

Sample code:

Re-encode the payload exactly as it was signed

The signature covers a canonical encoding of the payload: object keys sorted alphabetically at every level, no whitespace, and <, > and & written as the escapes \u003c, \u003e and \u0026. Re-serialising with any other settings changes the bytes, so the digest changes and verification fails even though the signature is genuine. Go's encoding/json produces this form by default; the Java sample configures Jackson to match it.

package com.trustingsocial.example;

import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.CharacterEscapes;
import com.fasterxml.jackson.core.io.SerializedString;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;

public class VerifySignature {

  public static void main(String[] args) throws Exception {
    String input = "YOUR_DATA";
    String publicKeyPem =
        "-----BEGIN PUBLIC KEY-----\n"
            + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwC4C2c+nbI5GN3RseYUE\n"
            + "6xB1SObAk1HNFrs41sbBL1ZgMcYmXnuPrqXehzdqbBSYnWwFufLwFnFfAox7dInw\n"
            + "2olB8QCX9aOYXxcCX+T4JKJxYMuMC7+vuKVip22U+6WUTOaHqGAB2ZmsGPp7dpN4\n"
            + "Q+5duwiYam4izHwiFZ2bGmXbDqARtRlO+UnA8JMVE9LISQD1p238Id+AY3K9W8GQ\n"
            + "W3luLJttKZ0/ulJ54JsVxg5wF3EtohfcvVSJD/6rY0BLUINB5o9nLuGYkjpd+7V0\n"
            + "oB+o4YDMPMOTu8IJa0tbZg8HClr+/Rq3W4xle/NFS1YbLmp45MJ8APsbX9OkG+jM\n"
            + "JwIDAQAB\n"
            + "-----END PUBLIC KEY-----";

    ObjectMapper mapper = canonicalJsonMapper();

    // Extract signature and prepare data for verification
    Map<String, Object> rawData =
        mapper.readValue(input, new TypeReference<Map<String, Object>>() {});
    String signature = (String) rawData.remove("signature");
    String data = mapper.writeValueAsString(rawData);

    // Load the RSA public key PEM string (be careful with the \n, space, tab) or from file
    PublicKey publicKey = loadRSAPublicKeyString(publicKeyPem);
    // PublicKey publicKey = loadRSAPublicKey("key.pub");

    // Verify the signature
    if (verify(publicKey, data, signature)) {
      System.out.println("Signature is valid!");
    } else {
      System.out.println("Error verifying signature: signature does not match");
    }
  }

  /*
   * The signature was produced over the canonical encoding of the payload: object keys sorted
   * alphabetically at every level, <, > and & escaped rather than emitted raw, and every number
   * spelled exactly as it arrived. Jackson does none of these by default, and any difference in
   * the bytes changes the SHA-1 digest, so this configuration is what makes the re-encoded
   * payload byte-identical to the one that was signed.
   */
  private static ObjectMapper canonicalJsonMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
    // Read decimals as BigDecimal, which keeps their exact digits. Parsing them into a double
    // instead reformats on the way out — a score of 0.0005 comes back as 5.0E-4 — and the
    // signature then fails on a payload that is perfectly genuine.
    mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
    mapper.registerModule(exponentCaseModule());
    mapper.getFactory().setCharacterEscapes(new HtmlCharacterEscapes());
    return mapper;
  }

  /** BigDecimal prints an upper-case exponent (1E-7); the signed form uses lower case (1e-7). */
  private static SimpleModule exponentCaseModule() {
    SimpleModule module = new SimpleModule();
    module.addSerializer(
        BigDecimal.class,
        new JsonSerializer<BigDecimal>() {
          @Override
          public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider)
              throws IOException {
            gen.writeNumber(value.toString().replace('E', 'e'));
          }
        });
    return module;
  }

  private static class HtmlCharacterEscapes extends CharacterEscapes {
    private static final long serialVersionUID = 1L;

    private final int[] escapes = CharacterEscapes.standardAsciiEscapesForJSON();

    HtmlCharacterEscapes() {
      escapes['<'] = CharacterEscapes.ESCAPE_CUSTOM;
      escapes['>'] = CharacterEscapes.ESCAPE_CUSTOM;
      escapes['&'] = CharacterEscapes.ESCAPE_CUSTOM;
    }

    @Override
    public int[] getEscapeCodesForAscii() {
      return escapes;
    }

    @Override
    public SerializableString getEscapeSequence(int ch) {
      switch (ch) {
        case '<':
          return new SerializedString("\\u003c");
        case '>':
          return new SerializedString("\\u003e");
        case '&':
          return new SerializedString("\\u0026");
        default:
          return null;
      }
    }
  }

  private static PublicKey loadRSAPublicKey(String filePath) throws Exception {
    return loadRSAPublicKeyString(
        new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8));
  }

  private static PublicKey loadRSAPublicKeyString(String pem) throws Exception {
    String base64 =
        pem.replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replaceAll("\\s", "");
    X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(base64));
    return KeyFactory.getInstance("RSA").generatePublic(spec);
  }

  /** RSASSA-PKCS1-v1_5 over a SHA-1 digest — the scheme the signature was produced with. */
  private static boolean verify(PublicKey publicKey, String msg, String signature)
      throws Exception {
    Signature verifier = Signature.getInstance("SHA1withRSA");
    verifier.initVerify(publicKey);
    verifier.update(msg.getBytes(StandardCharsets.UTF_8));
    return verifier.verify(Base64.getDecoder().decode(signature));
  }
}

III.2 Call HTTP Request

  • Use-case: Client's Backend call to TrustingSocial's Backend.
  • The client use the API Application Detail to get the user flow result.

III.3 Redirect URL

  • Use-case: TrustingSocial's Website redirect to Client's Website.
  • After the user finishes, they can be redirected to a URL you provide. Your backend can then call our API to retrieve the results. See details III.2