SDK v1.1.0 · song ngữ (vi/en), Stateless, có chữ ký chống can thiệpbilingual (vi/en), Stateless, tamper-proof signature · 📄 Markdown (cho LLM/đọc thôfor LLM/raw)
1. Bắt đầu nhanhQuick start
Nhúng SDK và gọi HQVeKYC.open(). Hàm trả về một Promise chứa kết quả; đồng thời có thể dùng callback onComplete.
Include the SDK and call HQVeKYC.open(). It returns a Promise with the result; you may also use the onComplete callback.
<script src="https://ekyc.hqv.biz/sdk/hqv-ekyc.js"></script>
<script>
const kq = await HQVeKYC.open({
apiBase: "https://ekyc.hqv.biz",
apiKey: "hqv_live_xxxxxxxx", // tenant key / khóa tenant
lang: "vi", // "vi" | "en"
appName: { vi: "Client Portal", en: "Client Portal" }
});
if (kq.status === "success" && kq.decision === "PASS") {
// kq.document, kq.face, kq.liveness, kq.files, kq.attestation ...
}
</script>
Yêu cầu HTTPS (để dùng camera) và domain phải được khai báo trong tenant (xem mục 9).
Requires HTTPS (for the camera) and the domain must be registered for the tenant (see §9).
2. Tùy chọnOptions
| Option | Type · Default | Mô tảDescription |
|---|---|---|
| apiBase | string · location.origin | URL máy chủ eKYC.eKYC server URL. |
| apiKey | string · "" | Khóa của tenant. Bắt buộc (trừ khi dùng requestHook).Tenant key. Required (unless using requestHook). |
| lang | "vi" | "en" · "vi" | Ngôn ngữ khởi tạo. Người dùng có thể đổi trong modal.Initial language. User can switch in the modal. |
| requireConsent | bool · true | Bắt buộc đồng ý điều khoản trước khi xử lý.Require consent before processing. |
| termsUrl | string · apiBase+"/terms" | Link điều khoản eKYC.eKYC terms link. |
| appName | {vi,en} · null | Tên ứng dụng của bạn (hiển thị).Your app name (shown). |
| appTerms | object · null | Điều khoản riêng của app: {label:{vi,en}, url:{vi,en}} → thêm 1 checkbox đồng ý.Your own terms: {label:{vi,en}, url:{vi,en}} → adds a second consent checkbox. |
| captureBack | bool · true | Có chụp mặt sau giấy tờ không.Capture document back side. |
| requireLiveness | bool · true | Bắt buộc selfie + liveness + so khớp mặt.Require selfie + liveness + face match. |
| checkExpiry | bool · true | Giấy tờ hết hạn → FAIL.Expired document → FAIL. |
| allowUpload | bool · true | Cho phép TẢI ảnh giấy tờ từ thiết bị. Selfie luôn bắt buộc camera live.Allow uploading document images. Selfie is always live-camera only. |
| maxRetries | int · 3 | Số lần làm lại mỗi bước khi fail.Retries allowed per failing step. |
| autoCapture | bool · true | Tự động chụp selfie khi bắt nét khuôn mặt (đúng 1 mặt, giữa khung, đủ nét, ổn định ~1s). Cần FaceDetector API (Chrome/Android); trình duyệt không hỗ trợ thì vẫn chụp tay bình thường.Auto-capture the selfie when the face is in focus (single face, centered, sharp, stable ~1s). Needs the FaceDetector API (Chrome/Android); falls back to the manual button elsewhere. |
| returnFiles | bool · true | Trả kèm files[] (ảnh + report + JSON).Include files[] (images + report + JSON). |
| maxEdge / jpegQuality | 1600 · 0.9 | Chuẩn resize ảnh trước khi gửi.Image resize standard before upload. |
| theme | object · null | Màu thương hiệu: {primary, secondary, success, danger, bg, card, fg, line, muted}. VD bên dưới.Brand colors: {primary, secondary, success, danger, bg, card, fg, line, muted}. Example below. |
| requestHook | fn(path, formData) · null | Tự vận chuyển request (vd proxy qua backend để giấu key). Trả về JSON envelope.Custom transport (e.g. proxy via your backend to hide the key). Returns the JSON envelope. |
| onComplete / onCancel | fn · null | Callback khi xong / khi hủy.Callbacks on complete / cancel. |
2b. Tùy biến màu thương hiệuBrand theming
await HQVeKYC.open({
apiBase: "https://ekyc.hqv.biz", apiKey: "hqv_live_xxxxxxxx",
theme: {
primary: "#00a867", // nút chính, thanh bước, link / primary buttons, steps, links
secondary: "#004f30", // thanh đang xử lý / processing bar
success: "#348D00", // PASS, còn hạn, khớp / PASS, valid, match
danger: "#ED542C", // FAIL, hết hạn, lỗi / FAIL, expired, errors
bg: "#1a5093", // nền modal (mặc định) / modal background (default)
// (tùy chọn / optional) card, fg, line, muted, onPrimary
}
});
3. Luồng xử lýFlow
Đồng ý điều khoản → chụp mặt trước → mặt sau → bóc tách + kiểm tra hiệu lực → chụp khuôn mặt (liveness) → kết quả. Fail ở bước nào → yêu cầu làm lại đúng bước đó (tối đa maxRetries).
Consent → capture front → back → extract + validity check → capture face (liveness) → result. On failure, retry that exact step (up to maxRetries).
4. Kết quả trả vềResult object
{
"status": "success" | "cancelled" | "declined",
"decision": "PASS" | "FAIL",
"reasons": ["face_mismatch" | "liveness_failed" | "document_expired" | "ocr_unreadable"],
"consent": { "agreed": true, "at": "ISO-8601", "version": "1.0", "lang": "vi",
"ekyc_terms": true, "app_terms": true },
"document": { "doc_type": "CCCD|CC|PASSPORT|UNSUPPORTED",
"id","name","dob","sex","nationality","home","address","doe" },
"validity": { "doe": "dd/mm/yyyy", "expired": false, "days_to_expiry": 495 },
"raw_text": { "front": ["...dòng OCR..."], "back": ["..."] },
"face": { "match": true, "prob": 77.4, "similarity": 0.548,
"threshold": 0.35, "quality_ok": true },
"liveness": { "liveness": "success", "is_live": true, "score": 0.99, "threshold": 0.55 },
"image_hashes": { "id_card": "sha256hex", "selfie": "sha256hex" },
"attestation": { "payload": { ... }, "signed": {
"alg": "HMAC-SHA256", "key_id": "hqv-ekyc-k1",
"signed_at": "ISO-8601", "signature": "hex" } },
"params": { "faceThreshold":0.35, "livenessThreshold":0.55, "maxRetries":3, "lang":"vi", ... },
"retries": { "doc": 0, "selfie": 1 },
"files": [ /* xem mục 5 */ ],
"images": { "front":"data:...", "back":"data:...", "selfie":"data:..." },
"timestamp": "ISO-8601", "sdk": "1.1.0"
}
4b. Dữ liệu mẫu đầy đủ (PASS)Full sample data (PASS)
{
"status": "success",
"decision": "PASS",
"reasons": [],
"consent": { "agreed": true, "at": "2026-07-10T14:32:07.512Z", "version": "1.0",
"lang": "vi", "ekyc_terms": true, "app_terms": true },
"document": {
"doc_type": "CCCD",
"id": "012345678999",
"name": "NGUYỄN VĂN A",
"dob": "01/01/1990",
"sex": "Nam",
"nationality": "Việt Nam",
"home": "Phường Bến Nghé, Quận 1, TP. Hồ Chí Minh",
"address": "123 Nguyễn Huệ, Phường Bến Nghé, Quận 1, TP. Hồ Chí Minh",
"doe": "14/07/2026"
},
"validity": { "doe": "14/07/2026", "expired": false, "days_to_expiry": 4 },
"raw_text": {
"front": ["CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM","CĂN CƯỚC CÔNG DÂN",
"Số / No.: 012345678999","Họ và tên / Full name:","NGUYỄN VĂN A",
"Ngày sinh / Date of birth: 01/01/1990","Giới tính / Sex: Nam",
"Quốc tịch / Nationality: Việt Nam","Có giá trị đến: 14/07/2026"],
"back": ["Nơi thường trú: 123 Nguyễn Huệ, Phường Bến Nghé, Quận 1, TP. Hồ Chí Minh"]
},
"face": { "match": true, "prob": 77.41, "similarity": 0.5482,
"threshold": 0.35, "faces_a": 2, "faces_b": 1, "quality_ok": true },
"liveness": { "liveness": "success", "is_live": true, "score": 0.9909, "threshold": 0.55 },
"image_hashes": {
"id_card": "3b8f0c9a1d7e5f42a6b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2",
"selfie": "a17c4e2b9d6f18305c7e2a4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d4"
},
"attestation": {
"payload": {
"tenant_id": "your_tenant_id",
"decision": "PASS", "reasons": [],
"ocr": { "doc_type": "CCCD", "fields": { "...": "..." }, "validity": { "...": "..." } },
"validity": { "doe": "14/07/2026", "expired": false, "days_to_expiry": 4 },
"liveness": { "is_live": true, "score": 0.9909, "threshold": 0.55 },
"face_verification": { "match": true, "similarity": 0.5482, "threshold": 0.35 },
"image_hashes": { "id_card": "3b8f...e1f2", "selfie": "a17c...b2d4" }
},
"signed": {
"alg": "HMAC-SHA256", "key_id": "hqv-ekyc-k1",
"signed_at": "2026-07-10T14:32:08.001Z",
"signature": "4f2a1c8e5b7d9036a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8"
}
},
"params": { "maxEdge": 1600, "jpegQuality": 0.9, "faceThreshold": 0.35,
"livenessThreshold": 0.55, "requireLiveness": true, "checkExpiry": true,
"maxRetries": 3, "lang": "vi" },
"retries": { "doc": 0, "selfie": 1 },
"files": [
{ "kind": "id_front", "name": "id_front.jpg", "mime": "image/jpeg",
"size": 184213, "dataUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..." },
{ "kind": "id_back", "name": "id_back.jpg", "mime": "image/jpeg",
"size": 172940, "dataUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..." },
{ "kind": "selfie", "name": "selfie.jpg", "mime": "image/jpeg",
"size": 156802, "dataUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..." },
{ "kind": "report", "name": "ekyc_report.png", "mime": "image/png",
"size": 98123, "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUh..." },
{ "kind": "analysis", "name": "ekyc_result.json", "mime": "application/json",
"size": 2456, "dataUrl": "data:application/json;base64,eyJkZWNpc2lvbi...",
"data": { "decision": "PASS", "document": { "...": "..." },
"attestation": { "...": "..." } } }
],
"images": {
"front": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...",
"back": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...",
"selfie": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
},
"timestamp": "2026-07-10T14:32:08.010Z",
"sdk": "1.1.0"
}
Lưu ý: dataUrl ở trên đã rút gọn (base64 thật rất dài). tenant_id nằm trong attestation.payload.
Note: dataUrl above is truncated (real base64 is long). tenant_id is inside attestation.payload.
5. files[] — để lưu vào hệ thống của bạnfiles[] — to store in your system
Mỗi phần tử: { kind, name, mime, size, dataUrl, data? }. Các kind:
Each item: { kind, name, mime, size, dataUrl, data? }. The kind values:
| kind | mime | Nội dungContent |
|---|---|---|
| id_front / id_back | image/jpeg | Ảnh giấy tờ đã chụp/tải.Captured/uploaded document images. |
| selfie | image/jpeg | Ảnh khuôn mặt.Facial image. |
| report | image/png | 1 ảnh tổng hợp (thông tin + tỷ lệ khớp + kết luận).A single summary image (info + match + verdict). |
| analysis | application/json | Toàn bộ dữ liệu phân tích + chữ ký (cũng có ở .data).Full analysis data + signature (also at .data). |
6. Xác minh chữ ký (chống can thiệp)Verifying the signature (anti-tamper)
Nên xác minh ở BACKEND của bạn, không tin dữ liệu từ trình duyệt. Chữ ký do máy chủ eKYC tạo (HMAC-SHA256) bằng signing_key chia sẻ. Hai bước: (a) so image_hashes với sha256 của ảnh nhận được; (b) verify HMAC.
Verify on YOUR backend, don't trust browser data. The signature is created by the eKYC server (HMAC-SHA256) using the shared signing_key. Two steps: (a) compare image_hashes with sha256 of received images; (b) verify the HMAC.
Canonical = RFC 8785 (JCS), key_id = "hqv-ekyc-jcs". JCS dùng cách viết SỐ của JavaScript (1.0→1), sort key, UTF-8 → mọi client JS/PHP tái tạo CHÍNH XÁC. Chuỗi ký = JCS({ "payload": <body bỏ signed>, "signed_at": …, "key_id": … }).
Canonical = RFC 8785 (JCS), key_id = "hqv-ekyc-jcs". JCS uses JavaScript number formatting (1.0→1), sorted keys, UTF-8 → every JS/PHP client reproduces it exactly. Signed string = JCS({ "payload": <body minus signed>, "signed_at": …, "key_id": … }).
Python (pip install rfc8785)
import hashlib, hmac, rfc8785
def verify(obj: dict, signing_key: str) -> bool:
signed = obj.get("signed") or obj["attestation"]["signed"]
payload = obj.get("attestation", {}).get("payload") or {k:v for k,v in obj.items() if k!="signed"}
msg = rfc8785.dumps({"payload": payload, "signed_at": signed["signed_at"], "key_id": signed["key_id"]})
expect = hmac.new(signing_key.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expect, signed["signature"])
# + kiểm ảnh: sha256(file_bytes) == obj["image_hashes"]["id_card"] / ["selfie"]
TypeScript / Node (npm i canonicalize)
import canonicalize from "canonicalize";
import { createHmac, timingSafeEqual } from "crypto";
export function verify(obj: any, signingKey: string): boolean {
const signed = obj.signed ?? obj.attestation?.signed;
const { signed: _omit, ...rest } = obj;
const payload = obj.attestation?.payload ?? rest;
const msg = canonicalize({ payload, signed_at: signed.signed_at, key_id: signed.key_id })!;
const expect = createHmac("sha256", signingKey).update(msg, "utf8").digest("hex");
const a = Buffer.from(expect), b = Buffer.from(signed.signature);
return a.length === b.length && timingSafeEqual(a, b);
}
PHP (JCS thuần, không cần thư viện)
function jcs($v): string {
if (is_array($v)) return '['.implode(',', array_map('jcs', $v)).']'; // JSON array
if ($v instanceof stdClass) { // JSON object
$o = (array)$v; ksort($o, SORT_STRING); // sort key
$p = [];
foreach ($o as $k => $val)
$p[] = json_encode((string)$k, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES).':'.jcs($val);
return '{'.implode(',', $p).'}';
}
// scalar: PHP mặc định 1.0->"1", 0.5482->"0.5482" (khớp JCS). ĐỪNG bật JSON_PRESERVE_ZERO_FRACTION.
return json_encode($v, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
}
function verifyEkyc(string $rawBody, string $signingKey): bool {
$o = json_decode($rawBody); // stdClass -> giữ {} khác []
$signed = $o->signed ?? $o->attestation->signed;
$payload = $o->attestation->payload ?? (function($x){ $c = clone $x; unset($c->signed); return $c; })($o);
$msg = jcs((object)['payload' => $payload, 'signed_at' => $signed->signed_at, 'key_id' => $signed->key_id]);
return hash_equals(hash_hmac('sha256', $msg, $signingKey), $signed->signature);
}
json_decode phải cho ra stdClass (KHÔNG dùng true) để giữ {}≠[]. Bản cũ key_id="hqv-ekyc-k1" (json.dumps Python) đã ngừng.
JCS removes the float pitfall (JS/PHP format numbers identically to the server). Only PHP note: json_decode must yield stdClass (do NOT pass true) to keep {}≠[]. Old key_id="hqv-ekyc-k1" (Python json.dumps) is retired.
Chính sách ingest khuyến nghị (không chặn khách thật): chữ ký hợp lệ + ảnh khớp image_hashes → tin, ingest thẳng. Chữ ký sai/thiếu → KHÔNG trả 422; bỏ mọi claim của SDK, server tự re-verify bằng chính ảnh đã nhận rồi dùng kết quả server. Kẻ giả mạo không lợi gì; khách thật không kẹt vì trục trặc kỹ thuật.
Recommended ingest policy (never block real users): valid signature + images match image_hashes → trust & ingest. Invalid/missing signature → do not return 422; drop all SDK claims and re-verify server-side using the received images, then use the server result. Forgers gain nothing; genuine users never get stuck on a technical hiccup.
7. REST API (gọi trực tiếp / server-to-serverdirect / server-to-server)
Header bắt buộc: X-API-KEY: <tenant key>. Envelope: { statusCode, message, object } (message IDG-00000000 = OK). Gọi từ backend không cần Origin. Thử ngay tại tab 🧪 Test API.
Required header: X-API-KEY: <tenant key>. Envelope: { statusCode, message, object } (message IDG-00000000 = OK). Backend calls need no Origin. Try it in the 🧪 API Tester tab.
| Method · Path | multipart | object |
|---|---|---|
| POST /ai/v1/ocr/id | image | trường CCCD/Căn cước + validity + raw_textID fields + validity + raw_text |
| POST /ai/v1/ocr/passport | image | MRZ |
| POST /ai/v1/ocr/document | file | OCR tài liệu tự do: ảnh hoặc PDF nhiều trang → văn bản thô theo trang (pages[].text). Tính năng bật riêng theo tenant + quota ngày riêng — liên hệ quản trị.Free-form document OCR: image or multi-page PDF → raw text per page (pages[].text). Per-tenant opt-in feature with its own daily quota — contact the administrator. |
| POST /ai/v1/ocr/document/async | file | Như trên nhưng BẤT ĐỒNG BỘ: trả job_id ngay (202), xử lý qua hàng đợi; kết quả cache trong RAM 1 giờ (Stateless). Nộp lại đúng file → trả job cũ (cached=true).Same as above but ASYNC: returns job_id immediately (202), processed via queue; result cached in RAM for 1h (Stateless). Re-submitting the same file returns the prior job (cached=true). |
| GET /ai/v1/jobs/{job_id} | — | Poll job: status = queued | processing | done | error; khi done có result (đã ký).Poll a job: status = queued | processing | done | error; when done, includes signed result. |
| POST /ai/v1/chat | message, history?, file? | Chat với trợ lý AI on-device (không dữ liệu nào rời máy). Gửi kèm file (ảnh/PDF) thì server OCR trước rồi trợ lý làm theo yêu cầu trên văn bản đó (giải thích, tóm tắt, bóc trường...). history = JSON các lượt trước (server không lưu hội thoại). Tính năng bật riêng theo tenant.Chat with the on-device AI assistant (no data leaves the machine). Attach a file (image/PDF) and the server OCRs it first so the assistant can act on the text (explain, summarize, extract...). history = JSON of prior turns (server stores no conversations). Per-tenant opt-in. |
| POST /ai/v1/face/compare | img1, img2 | match, prob, similarity, quality_ok |
| POST /ai/v1/liveness | image | liveness, is_live, score |
| POST /ai/v1/verify | id_card, selfie | quyết định tổng hợp + chữ ký + tenant_idfull decision + signature + tenant_id |
| GET /ready | — | mode, zero_storage, model status |
curl -X POST https://ekyc.hqv.biz/ai/v1/verify \
-H "X-API-KEY: hqv_live_xxxxxxxx" \
-F id_card=@front.jpg -F selfie=@selfie.jpg
7.1 · OCR tài liệu bất đồng bộ (queue + cache)Async document OCR (queue + cache)
Dành cho tài liệu lớn (PDF nhiều trang) hoặc khi app không muốn giữ kết nối chờ. Nộp file → nhận job_id ngay → poll đến khi done. Job và kết quả chỉ nằm trong RAM, tự xóa sau 1 giờ (Stateless — không ghi đĩa). Nộp lại đúng file đó (SHA-256 trùng, cùng tenant) trong 1 giờ → trả ngay job cũ với cached=true, không xếp hàng lại.
For large documents (multi-page PDFs) or when your app shouldn't hold a connection open. Submit the file → get a job_id immediately → poll until done. Jobs and results live in RAM only and auto-expire after 1 hour (Stateless — nothing touches disk). Re-submitting the identical file (same SHA-256, same tenant) within the hour returns the prior job instantly with cached=true.
# 1) Nộp / Submit → 202
curl -X POST https://ekyc.hqv.biz/ai/v1/ocr/document/async \
-H "X-API-KEY: hqv_live_xxxxxxxx" -F file=@hopdong.pdf
# {"object":{"job_id":"c4db57adf35e4866","status":"queued","sha256":"71d7...","expires_in_sec":3600}}
# 2) Poll (1-2s/lần) / Poll every 1-2s
curl https://ekyc.hqv.biz/ai/v1/jobs/c4db57adf35e4866 -H "X-API-KEY: hqv_live_xxxxxxxx"
# status: queued → processing → done | error
# {"object":{"status":"done","ms":828,"result":{ kind, pages[], id_card?, signed }}}
Trạng thái: queued (đang chờ, kèm queue_size) → processing → done (kèm result đã ký + ms) hoặc error. 404 = sai id, khác tenant, hoặc quá TTL. 429 = hàng đợi đầy hoặc hết quota ngày. Quota dùng chung với /ocr/document, tính lúc nộp.States: queued (waiting, includes queue_size) → processing → done (with signed result + ms) or error. 404 = wrong id, different tenant, or past TTL. 429 = queue full or daily quota exhausted. Quota is shared with /ocr/document, charged at submit time.
7.2 · Chat với trợ lý AI on-deviceOn-device AI assistant chat
Trợ lý chạy ngay trên máy chủ HQV (Gemma 4 qua MLX, Apple Silicon) — câu hỏi, tài liệu và câu trả lời không rời máy, không gọi cloud AI nào. Server không lưu hội thoại: app tự giữ lịch sử và gửi kèm mỗi lượt qua history. Đính kèm file (ảnh/PDF) thì server OCR trước rồi trợ lý làm theo yêu cầu trên văn bản đó — ví dụ user gửi biên lai và hỏi "kiểm tra giúp tôi số tiền".
The assistant runs on the HQV server itself (Gemma 4 via MLX on Apple Silicon) — prompts, documents and answers never leave the machine; no cloud AI is called. The server stores no conversations: your app keeps the history and sends it each turn via history. Attach a file (image/PDF) and the server OCRs it first so the assistant can act on that text — e.g. the user sends a receipt and asks "check the amount for me".
curl -X POST https://ekyc.hqv.biz/ai/v1/chat \
-H "X-API-KEY: hqv_live_xxxxxxxx" \
-F 'message=Giải thích giúp tôi biên lai này' \
-F 'history=[{"role":"user","content":"chào"},{"role":"assistant","content":"Chào anh/chị!"}]' \
-F file=@bienlai.jpg
# {"object":{"reply":"Đây là biên lai chuyển khoản...","model":"...gemma-4-e4b-it-4bit",
# "ocr":{"kind":"image","ocr_pages":1,"id_card":null},"signed":{...}}}
Lượt gọi đầu tiên sau khi service khởi động sẽ chậm hơn (~10-20s nạp model vào RAM); các lượt sau nhanh. history giữ tối đa 8 lượt gần nhất. Trả lời tối đa ~1024 token. Tính năng bật riêng theo tenant + quota ngày riêng (mặc định 300 lượt/ngày) — liên hệ quản trị. 403 = tenant chưa bật; 429 = hết quota.The first call after service startup is slower (~10-20s to load the model into RAM); later turns are fast. history keeps the last 8 turns. Replies are capped at ~1024 tokens. Per-tenant opt-in with its own daily quota (default 300/day) — contact the administrator. 403 = feature not enabled; 429 = quota exhausted.
7.3 · eKYC qua LINK/QR (chuyển thiết bị) + callbackeKYC via LINK/QR (device handoff) + callback
Khi người dùng ở trên máy tính không có camera, hoặc bạn muốn gửi link cho khách tự làm eKYC (không cần đăng nhập). Tạo phiên → nhận link + mã QR → khách mở trên điện thoại → kết quả poll được và/hoặc push về callback của bạn (đã ký HMAC). App tự quyết cách dùng.
For users on a computer without a camera, or to send a link to your customer to do eKYC (no login). Create a session → get a link + QR → they open it on a phone → the result is pollable and/or pushed to your callback (HMAC-signed). Your app decides how to use it.
| Method · Path | Auth | object |
|---|---|---|
| POST /ai/v1/sessions | X-API-KEY | tạo phiên → {session_id, url, qr, poll_url, expires_in_sec}create → {session_id, url, qr, poll_url, expires_in_sec} |
| GET /ai/v1/sessions/{id} | X-API-KEY | poll → {status, result?, callback_status}poll → {status, result?, callback_status} |
| GET /s/{token} | — | trang eKYC nhúng cho khách (mở bằng link/QR, không cần đăng nhập)hosted eKYC page (opened via link/QR, no login) |
# 1) Tạo phiên (tùy chọn callback_url + options SDK cho trang nhúng)
curl -X POST https://ekyc.hqv.biz/ai/v1/sessions \
-H "X-API-KEY: hqv_live_xxxxxxxx" -H "Content-Type: application/json" \
-d '{"callback_url":"https://app.cua-ban.vn/ekyc/callback",
"options":{"captureBack":true,"activeLiveness":"silent"}, "ttl_sec":900}'
# {"object":{"session_id":"dsXnFg96","url":"https://ekyc.hqv.biz/s/sess_...",
# "qr":"data:image/png;base64,iVBORw0K...","poll_url":"/ai/v1/sessions/dsXnFg96"}}
# 2) Hiện url + qr (data-URI PNG) cho khách, rồi poll đến khi status=done
curl https://ekyc.hqv.biz/ai/v1/sessions/dsXnFg96 -H "X-API-KEY: hqv_live_xxxxxxxx"
options = mọi tùy chọn HQVeKYC.open() (mục 2). Stateless: máy chủ lưu kết quả theo id, KHÔNG kèm ảnh (chỉ để poll, tự xóa sau TTL 900s); ảnh chỉ gửi ĐẦY ĐỦ một lần qua callback rồi mất.options = any HQVeKYC.open() option (§2). Stateless: the server keeps the result by id, WITHOUT images (poll only, auto-deleted after 900s TTL); images are sent in FULL only once via the callback, then gone.
Callback (webhook): khi khách xong, máy chủ POST tới callback_url với header X-HQV-Signature (HMAC-SHA256 của body, khóa = signing_key) + X-HQV-Session; body { session_id, result{ decision, document, files[](có ảnh), attestation } }. Verify: hmac_sha256(signing_key, raw_body) == X-HQV-Signature.
Callback (webhook): when done, the server POSTs to callback_url with X-HQV-Signature (HMAC-SHA256 of the body, key = signing_key) + X-HQV-Session; body { session_id, result{ decision, document, files[](with images), attestation } }. Verify: hmac_sha256(signing_key, raw_body) == X-HQV-Signature.
SDK máy tính — tự hiện QR & chờDesktop SDK — auto QR & wait
const kq = await HQVeKYC.handoff({
apiBase: "https://ekyc.hqv.biz", apiKey: "hqv_live_xxxxxxxx",
options: { activeLiveness: "silent" } // tùy chọn SDK cho trang trên điện thoại
});
// hiện modal QR + link, poll đến khi điện thoại làm xong -> trả kết quả
if (kq.status === "success" && kq.decision === "PASS") { /* ... */ }
8. Trạng thái & lỗiStatus & errors
| TrườngField | Giá trịValues |
|---|---|
| status | success · cancelled (người dùng bấm ✕user closed) · declined (không đồng ý điều khoảnconsent declined) |
| decision | PASS · FAIL |
| reasons | face_mismatch · liveness_failed · document_expired · ocr_unreadable |
| HTTP 403 | key sai hoặc Origin không được phépbad key or Origin not allowed |
| HTTP 413 / 400 | ảnh quá lớn / ảnh hỏngimage too large / invalid |
9. Bảo mật & multi-tenantSecurity & multi-tenant
- Khóa gắn với domain: mỗi tenant khai báo
allowed_origins. Gọi từ trình duyệt ở domain khác → 403. Vì vậy key nhúng trong trang web chỉ chạy trên domain đã đăng ký. - Domain-bound key: each tenant declares
allowed_origins. Browser calls from another domain → 403. So an embedded key only works on the registered domain. - Kín hơn: gọi qua backend của bạn (server-to-server, không có Origin) và dùng
requestHookđể không lộ key ra client. - Stronger: call via your backend (server-to-server, no Origin) and use
requestHookto keep the key off the client. - Stateless: máy chủ eKYC không lưu ảnh/PII; ứng dụng của bạn là bên quyết định lưu trữ.
- Stateless: the eKYC server stores no images/PII; your app decides on storage.
Tự đăng ký tenant & quản lý API key tại /portal (tenant mới cần quản trị viên duyệt, có email báo khi duyệt xong).Self-register a tenant & manage your API key at /portal (new tenants require operator approval; you get an email once approved). · Terms & Data Policy