Skip to content

Backend Integration: OCR & Face Verify APIs

Mục tiêu bài học

  • Hiểu API architecture trong eKYC
  • Nắm OCR Front/Back APIs
  • Biết cách Face Verify API
  • Học Check2FA và Ward Search APIs
  • Hiểu UpdateInfoSso API integration

Tổng quan API

Kiến trúc

Frontend (Vue 3)

Portal-Server (Express/TypeScript)

External APIs (iNET SSO, EKYC Service)

URL cơ sở

typescript
// Endpoint Portal-Server
const API_BASE = "/api/v1";

// Dịch vụ bên ngoài
const ENDPOINT_EKYC = process.env.ENDPOINT_EKYC; // Dịch vụ EKYC
const ENDPOINT_MY_INET = process.env.ENDPOINT_MY_INET; // my.inet.vn
const ENDPOINT_JAVA_INET = process.env.ENDPOINT_JAVA_INET; // Backend Java

OCR APIs

Front Card OCR

Endpoint: POST /api/v1/ekyc/ocr-front

Request:

typescript
interface IOcrFrontRequest {
  image: string; // Base64 encoded image
}

Response:

typescript
interface IOcrFrontResponse {
  status: "success" | "error";
  data?: {
    id_number: string;
    name: string;
    birthday: string; // "DD/MM/YYYY"
    sex: string; // "Nam" | "Nữ"
    nationality: string;
    place_of_residence: string; // Full address
    address?: string; // Alternative address field
    province?: string;
    precinct?: string; // Ward/commune
  };
  error?: string;
}

Usage:

typescript
async function getOcrData(base64Image: string): Promise<IOcrFrontResponse> {
  const response = await ekycService.ocrFront({
    image: base64Image,
  });
  return response.data;
}

Back Card OCR

Endpoint: POST /api/v1/ekyc/ocr-back

Request:

typescript
interface IOcrBackRequest {
  image: string; // Base64 encoded image
}

Response:

typescript
interface IOcrBackResponse {
  status: "success" | "error";
  data?: {
    issue_date: string; // "DD/MM/YYYY"
    issue_place: string;
    // ... other fields
  };
  error?: string;
}

Face Verification API

Endpoint: POST /api/v1/ekyc/face-verify

Request:

typescript
interface IFaceVerifyRequest {
  front_card_image: string; // Base64
  face_image: string; // Base64
  back_card_image: string; // Base64
  front_ocr_data: IOcrFrontResponse["data"];
  back_ocr_data: IOcrBackResponse["data"];
}

Response:

typescript
interface IFaceVerifyResponse {
  verify_result: {
    is_same_person: boolean;
    similarity: number; // 0-1
  };
  face_anti_spoof_status: {
    status: "REAL" | "FAKE";
    confidence: number; // 0-1
  };
}

Usage:

typescript
async function verifyFace(
  frontCard: string,
  faceImage: string,
  backCard: string,
  frontOcr: any,
  backOcr: any
): Promise<IFaceVerifyResponse> {
  const response = await ekycService.faceVerify({
    front_card_image: frontCard,
    face_image: faceImage,
    back_card_image: backCard,
    front_ocr_data: frontOcr,
    back_ocr_data: backOcr,
  });
  return response.data;
}

Check2FA API

Endpoint: POST /api/v1/customer/check-2fa

Mục đích: Kiểm tra thông tin người dùng hiện tại, đặc biệt là addressLevelward.

Response:

typescript
interface ICheck2FAResponse {
  addressLevel: number | string; // 0 = chưa update
  ward?: string;
  // ... other user fields
}

Usage:

typescript
async function check2fa(): Promise<ICheck2FAResponse> {
  const response = await customerService.check2fa();
  const data =
    response.data?.values || response.data?.data || response.data || response;
  return data;
}

Ward Search APIs

Find Ward by Address

Endpoint: POST /api/v1/ward/find-by-address

Request:

typescript
interface IFindWardRequest {
  address: string; // Full address from OCR
}

Response:

typescript
interface IFindWardResponse {
  found: boolean;
  wards: Array<{
    wardName: string;
    districtName: string;
    provinceName: string;
    wardCode: string;
  }>;
}

Usage:

typescript
async function findWard(address: string): Promise<IFindWardResponse> {
  const response = await customerService.findWardByAddress({ address });
  const data =
    response.data?.values || response.data?.data || response.data || response;
  return data;
}

Check New VN Province Ward

Endpoint: POST /api/v1/ward/check-new-vn-province-ward

Mục đích: Tìm phường/xã mới sau sáp nhập (từ 01/07/2025).

Request:

typescript
interface ICheckNewWardRequest {
  query: string; // Old ward name
}

Response:

typescript
interface ICheckNewWardResponse {
  // Array of new ward options
  Array<{
    oldWard: string;
    newWard: string;
    newProvince: string;
  }>;
}

Usage:

typescript
async function checkNewWard(
  oldWardName: string
): Promise<ICheckNewWardResponse> {
  const response = await customerService.checkNewVnProvinceWard({
    query: oldWardName,
  });
  let data =
    response.data?.values || response.data?.data || response.data || response;
  if (!Array.isArray(data)) {
    data = [];
  }
  return data;
}

Province List API

Endpoint: POST /api/v1/category/province

Mục đích: Lấy danh sách tỉnh/thành phố để ánh xạ tên ↔ mã.

Response:

typescript
interface IProvinceListResponse {
  Array<{
    value: string; // Province code (e.g., "HNI", "HCM")
    name: string; // Province name (e.g., "Thành phố Hà Nội")
  }>;
}

Usage:

typescript
async function loadProvinceList(): Promise<IProvinceListResponse> {
  const response = await categoryService.listProvince();
  const data =
    response.data?.values || response.data?.data || response.data || response;
  return Array.isArray(data) ? data : [];
}

UpdateInfoSso API

Endpoint: POST /api/v1/customer/update-info-sso

Mục đích: Cập nhật thông tin người dùng sau khi xác thực khuôn mặt thành công.

Proxy Backend: Chuyển tiếp tới https://my.inet.vn/api/client/v1/account/updateinfosso

Request:

typescript
interface IUpdateInfoSsoRequest {
  email?: string; // Auto-filled from authenticated user if not provided
  fullname?: string; // From OCR
  address?: string; // Combined address detail + ward
  ward?: string; // New ward name
  country?: string; // Default: "VN"
  province?: string; // Province CODE (not name), e.g., "HNI"
  gender?: string; // "male" | "female" | "other"
  birthDay?: string; // "MM/DD/YYYY HH:mm:ss" format
  idNumber?: string; // From OCR
  phone?: string; // From OCR (if available)
}

Response:

typescript
interface IUpdateInfoSsoResponse {
  status: "success" | "error";
  message?: string;
  error?: string;
}

Backend Implementation:

typescript
// portal-server/src/services/client/UserService.ts
export const updateInfoSsoService = async function (
  body: IUpdateInfoSsoRequest
): Promise<any> {
  return await actionTryCatchPerformance(
    async () => {
      const response = await apiRequestInet(endpointMyInet).post(
        "api/client/v1/account/updateinfosso",
        body
      );
      if (getNestedPropValue(response, "data")) {
        if (getNestedPropValue(response.data, "status") === "error") {
          return renderErrorResponse(response.data);
        }
        return response.data;
      } else {
        return response;
      }
    },
    { name: "updateInfoSsoService" }
  );
};

Frontend Usage:

typescript
// After successful face verification
if (isSamePerson(faceData)) {
  // Prepare update data
  const updateData: IUpdateInfoSsoRequest = {
    fullname: mergedFrontOcrData.name,
    address: fullAddress, // Combined detail + ward
    ward: wardName,
    country: "VN",
    province: provinceCode, // CODE, not name
    gender: genderMap[mergedFrontOcrData.sex], // "male" | "female" | "other"
    birthDay: formatBirthday(mergedFrontOcrData.birthday), // "MM/DD/YYYY 17:00:00"
    idNumber: mergedFrontOcrData.idNumber,
    phone: mergedFrontOcrData.phone,
  };

  // Call API
  await customerService.updateInfoSso(updateData);
}

Data Mapping:

typescript
// Gender mapping
const genderMap: { [key: string]: string } = {
  Nam: "male",
  Nữ: "female",
  Khác: "other",
};

// Birthday format: "DD/MM/YYYY" → "MM/DD/YYYY 17:00:00"
function formatBirthday(birthday: string): string {
  const dateStr = birthday.replace(/-/g, "/");
  const parts = dateStr.split("/");
  if (parts.length === 3) {
    const day = parts[0].padStart(2, "0");
    const month = parts[1].padStart(2, "0");
    const year = parts[2];
    return `${month}/${day}/${year} 17:00:00`;
  }
  return birthday;
}

// Province: Get code from name
function getProvinceCode(provinceName: string): string | null {
  const province = provinceList.value.find(
    (p) => normalizeText(p.name) === normalizeText(provinceName)
  );
  return province?.value || null;
}

Các mẫu xử lý lỗi

Xử lý timeout

typescript
try {
  const response = await ekycService.ocrFront({ image });
} catch (error: any) {
  const isTimeout =
    error.isTimeout === true ||
    error.code === "ECONNABORTED" ||
    error.message?.toLowerCase().includes("timeout");

  if (isTimeout) {
    // Switch to manual verification mode
    isManualVerificationMode.value = true;
    message.warning("OCR timeout, switching to manual mode");
  } else {
    throw error;
  }
}

Các biến thể cấu trúc phản hồi

typescript
// Xử lý các cấu trúc phản hồi khác nhau
function extractData(response: any): any {
  return (
    response.data?.values || response.data?.data || response.data || response
  );
}

// Sử dụng
const wardData = extractData(wardResponse);

Chiến lược thử lại

typescript
async function callWithRetry<T>(
  apiCall: () => Promise<T>,
  maxRetries = 3,
  delay = 1000
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await apiCall();
    } catch (error: any) {
      if (i === maxRetries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
  throw new Error("Max retries exceeded");
}

Thực hành tốt nhất

1. Luôn chuẩn hóa dữ liệu phản hồi

typescript
// Đừng giả định cấu trúc phản hồi
const data = extractData(response);

// Đảm bảo là mảng
if (!Array.isArray(data)) {
  data = [];
}

2. Xử lý các trường thiếu một cách nhẹ nhàng

typescript
// Sử dụng optional chaining và giá trị mặc định
const address =
  frontOcrData.value?.place_of_residence || frontOcrData.value?.address || "";

3. Ghi log các lời gọi API để debug

typescript
console.log("[API] Gọi updateInfoSso với dữ liệu:", updateData);
const response = await customerService.updateInfoSso(updateData);
console.log("[API] Phản hồi updateInfoSso:", response);

4. Không hiển thị lỗi cho các cập nhật nền

typescript
// Sau khi xác thực khuôn mặt thành công, updateInfoSso chạy ở nền
try {
  await customerService.updateInfoSso(updateData);
} catch (error: any) {
  console.error("[API] Lỗi cập nhật thông tin:", error);
  // Không hiển thị lỗi cho người dùng (xác thực khuôn mặt đã thành công)
}

Tổng kết

Luồng API:

  1. ✅ OCR Mặt trước/Mặt sau → Trích xuất dữ liệu người dùng
  2. ✅ Xác thực khuôn mặt → Xác minh danh tính
  3. ✅ Check2FA → Kiểm tra trạng thái địa chỉ
  4. ✅ Tìm phường/xã → Tìm kiếm phường/xã từ địa chỉ
  5. ✅ Kiểm tra phường/xã mới → Lấy các tùy chọn phường/xã sau sáp nhập
  6. ✅ Tải danh sách tỉnh → Ánh xạ tên ↔ mã
  7. ✅ UpdateInfoSso → Cập nhật thông tin người dùng sau xác thực

Nguyên tắc chính:

  • ✅ Xử lý các biến thể cấu trúc phản hồi
  • ✅ Chuẩn hóa dữ liệu trước khi sử dụng
  • ✅ Xử lý timeout một cách nhẹ nhàng
  • ✅ Ghi log các lời gọi API để debug
  • ✅ Không hiển thị lỗi cho các cập nhật nền

Bài học tiếp theo

Xử Lý Ward/District/Province
Review Step: Check2FA và Ward Logic

Internal documentation for iNET Portal