eKYC Overview
eKYC is CAKE's hosted identity-verification platform: a set of stateless HTTP APIs that upload an image once, then run ID card checks (detection, sanity, OCR, tampering) and face checks (sanity, liveness, comparison, indexing and search) against that image. This page covers authentication, the shared request/response contract, and the end-to-end verification flow.
INFO
Every sample in this documentation uses the same fictional identity — Nguyễn Văn A, NID 001xxxxxxxxx, user_id user_00000001, ref_id ref_00000001. Credentials, tokens and keys appear as <PLACEHOLDER>. None of it is real data; replace it all with your own values.
Prerequisites
Before calling any endpoint you need a tenant provisioned by CAKE and the credentials issued with it.
| What | Description |
|---|---|
| Access ID | A UUID that uniquely identifies your Tenant. Sent in the Authorization header. Provided by CAKE. |
| Access Key | The secret key used to sign every request and to verify every response signature. Provided by CAKE. |
| Base URL | Issued by CAKE together with your credentials, for both stage and production. |
| Tenant configuration | Thresholds, collections and enabled checks are configured per tenant by CAKE before go-live. |
| Client ID | X-Client-ID value issued alongside the credentials, sent on gateway-fronted requests. |
Contact CAKE Support to obtain access_id and access_key.
Samples throughout this documentation write the base URL as <BASE_URL> — substitute the value above.
WARNING
The Access Key is a server-side secret. Never embed it in a mobile app, a browser bundle, or any client the end user controls — sign requests from your backend only. Mobile clients authenticate with a short-lived JWT token instead (see below).
Core Concepts
Transaction Lifecycle
Every eKYC session runs inside a transaction, which moves through three states.
| Field | Type | Description |
|---|---|---|
NOT_VERIFIED | string | The transaction has been created and a token issued, but verification is not complete. |
VERIFIED | string | The authentication key was redeemed successfully. |
EXPIRED | string | The transaction was not completed before its token expired. |
| What | Description |
|---|---|
| Create Transaction | Your backend opens a transaction and receives the JWT the SDK will use. |
| Get Transaction | Reads the current status at any time. |
| Verify Transaction Key | Redeems the authentication key and closes the transaction. |
A transaction is optional for a bare image check, but required whenever you need a verifiable, auditable result your backend can trust.
Authentication
eKYC supports two authentication schemes, chosen by who is calling.
Option 1: HMAC authentication (backend to eKYC backend)
Server-to-server requests are authenticated with HMAC-SHA256, which proves both the integrity and the authenticity of the request body using a secret shared by your backend and CAKE.
Two headers are required on every request:
| Header | Description |
|---|---|
X-TV-Timestamp | Current timestamp in RFC3339 format. Example: 2019-04-21T18:00:15+07:00 |
Authorization | ATV <ACCESS_ID>:<SIGNATURE> |
The signature is a Base64-encoded HMAC-SHA256 of a canonical StringToSign:
StringToSign = HttpMethod + "\n" + UrlPath + "\n" + Timestamp + "\n" + HashedBody
# HttpMethod: POST, GET, PUT, PATCH or DELETE
# UrlPath: relative route path, excluding the domain. Example: /v1/images
# Timestamp: request time in RFC3339 format; must match the X-TV-Timestamp header
# HashedBody: hex_encode(SHA256(raw body))
# Example (body `{}`): 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
Signature = Base64(HmacSha256(AccessKeySecret, Utf8EncodingOf(StringToSign)))Reference implementation in JavaScript:
const CryptoJS = require("crypto-js");
const timestamp = new Date().toISOString(); // 2024-05-09T07:53:10.591Z
const access_key = "<ACCESS_KEY>";
const path = "/v1/create_ekyc_transaction";
const body = `{
"user_id": "user_00000001",
"ref_id": "ref_00000001"
}`;
function tokenize(method, path, timestamp, body, key) {
const hashed_body = CryptoJS.SHA256(body).toString(CryptoJS.enc.Hex);
const formatted = `${method}\n${path}\n${timestamp}\n${hashed_body}`;
const hmac = CryptoJS.HmacSHA256(formatted, key);
const hashed = hmac.toString(CryptoJS.enc.Hex);
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(hashed));
}
const authorize_token = tokenize("POST", path, timestamp, body, access_key);
// Send `timestamp` as X-TV-Timestamp and `authorize_token` as the signature — never log them.Sample signed request:
curl --location '<BASE_URL>/v1/create_ekyc_transaction' \
--header 'Authorization: ATV <ACCESS_ID>:<SIGNATURE>' \
--header 'X-TV-Timestamp: <TIMESTAMP>' \
--header 'Content-Type: application/json' \
--header 'X-Request-Id: <RANDOM_UUID>' \
--header 'X-Client-ID: <CLIENT_ID>' \
--data '{
"user_id": "user_00000001",
"ref_id": "ref_00000001"
}'INFO
The StringToSign uses the hex-encoded SHA256 of the raw body. Some CAKE tenants are provisioned with a Base64-encoded body hash instead — confirm which variant your tenant uses with the CAKE team before implementing.
Option 2: JWT token (mobile SDK to eKYC backend)
A JWT token is issued during the eKYC step for one specific user and ref_id, and the Mobile SDK presents it when calling the eKYC Server directly. The token stops working once it expires.
Treat the token as opaque: pass it to the SDK as-is, and never parse, log or store it.
Response Signing
Every response from the eKYC Server is signed with HMAC-SHA256 over the method, path, timestamp and hashed response body, so your backend can prove the payload really came from CAKE.
| Header | Description |
|---|---|
X-Signature-Timestamp | Signed timestamp in RFC3339 format. Example: 2019-04-21T18:00:15+07:00 |
X-Signature | Base64 HMAC token from the CAKE server. Example: <SIGNATURE> |
StringToSign = HttpMethod + "\n" + UrlPath + "\n" + Timestamp + "\n" + HashedBody
# HttpMethod: the method of the API that was called
# UrlPath: relative route path, excluding the domain. Example: /v1/images
# Timestamp: signed time in RFC3339 format, sent in X-Signature-Timestamp
# HashedBody: hex_encode(SHA256(raw response body))
Signature = Base64(HmacSha256(AccessKeySecret, Utf8EncodingOf(StringToSign)))WARNING
Recompute and compare X-Signature before you trust a verdict. An unverified response is an unauthenticated one, and eKYC verdicts drive onboarding decisions.
How It Works
A full eKYC session spans four actors: the mobile app, your backend, the eKYC Backend, and the AI engines behind it. The diagram below shows the canonical happy path.
sequenceDiagram
autonumber
participant App as Mobile App / SDK
participant BE as Your Backend
participant EKYC as eKYC Backend
participant AI as AI Engines
Note over App,EKYC: Phase 1 — Capture & upload
App->>BE: Captured ID card + portrait images
BE->>EKYC: POST /v1/images (multipart, HMAC signed)
EKYC-->>BE: image_id
Note over BE,AI: Phase 2 — ID card verification
BE->>EKYC: POST /v1/verify_id_card_sanity_sync
EKYC->>AI: Sanity model
AI-->>EKYC: verdict + score
EKYC-->>BE: card_sanity verdict
BE->>EKYC: POST /v1/read_id_card_info_sync
EKYC-->>BE: card_information (OCR / MRZ / QR / NFC)
BE->>EKYC: POST /v1/detect_id_card_tampering_sync
EKYC-->>BE: card_tampering verdict + details
Note over BE,AI: Phase 3 — Face verification
BE->>EKYC: POST /v1/verify_portrait_sanity_sync
EKYC-->>BE: portrait_sanity verdict
BE->>EKYC: POST /v1/verify_face_liveness_sync
EKYC-->>BE: is_live + score
BE->>EKYC: POST /v1/compare_faces_sync (portrait vs ID photo)
EKYC-->>BE: matched / unmatched / unsure + score
Note over BE,EKYC: Phase 4 — Dedup & decision
BE->>EKYC: POST /v1/search_faces_sync (duplicate check)
EKYC-->>BE: similar faces + scores
BE->>EKYC: POST /v1/index_faces_sync (enrol approved user)
EKYC-->>BE: status success
BE-->>App: Onboarding decision
Phase 1 — Capture and upload
- The mobile app or Mobile SDK captures the ID card front, the ID card back and a portrait selfie, then hands them to your backend.
- Your backend calls
POST /v1/imagesonce per image with the correctlabel. Each call returns animage_id. - Your backend keeps the
image_idvalues for the rest of the session — every downstream API accepts animage_idinstead of re-uploading bytes.
Phase 2 — ID card verification
POST /v1/verify_id_card_sanity_syncchecks that the card image is usable: sharp enough, complete, not a photocopy, not glared over the data fields. Run this first — a bad image poisons every check after it.POST /v1/read_id_card_info_syncextracts the card fields via OCR, MRZ, QR and (where available) NFC, each with a confidence score.POST /v1/detect_id_card_tampering_synclooks for forgery: an altered portrait, unusual fonts, cross-source mismatches between MRZ, QR and OCR, and invalid ID-number or expiry logic.
Phase 3 — Face verification
POST /v1/verify_portrait_sanity_syncchecks that the selfie is usable: one face, well framed, eyes open, not occluded.POST /v1/verify_face_liveness_syncconfirms a live person was in front of the camera rather than a photo or a screen replay.POST /v1/compare_faces_syncmatches the selfie against the face printed on the ID card, returningmatched,unmatchedorunsurewith a similarity score.
Phase 4 — Deduplication and decision
POST /v1/search_faces_syncsearches your face collection for anyone already enrolled with the same face — the standard duplicate-account and blocklist check.POST /v1/index_faces_syncadds the approved user's face to the collection so future sessions can find them.- Your backend combines every verdict into a single onboarding decision and returns it to the app. eKYC returns evidence; the accept/reject policy stays yours.
Shared Response Envelope
Every JSON endpoint returns the same outer shape.
{
"data": {
"request_id": "f77bf948-fce3-48f3-b61c-396e4cd9a89b",
"status": "success"
},
"errors": [],
"success": true,
"code": 0,
"message": "Success"
}| Field | Type | Description |
|---|---|---|
data | object | Endpoint-specific payload. Empty when the request failed before processing. |
data.status | string | success or failure — whether the request was processed successfully. |
data.request_id | string | Server-generated request ID. Quote it to CAKE Support when checking logs. |
errors | array | Detail of each failure. Empty on success. |
errors[].code | string | Machine-readable error code, e.g. image_not_found_exception. |
errors[].message | string | Human-readable description of the failure. |
errors[].detail | object | Optional context, e.g. field naming the invalid parameter. |
success | bool | Whether the call itself succeeded. |
code | int | Numeric status code — 0 on success. |
message | string | Status message. |
An HTTP 200 does not mean the check passed. Read success, then data.status, then the verdict.
Error Codes
Numeric codes
| Code | Message |
|---|---|
0 | Success |
2001002 | Unexpected error occurred while processing the external request. |
2001003 | Image sent to the server has no faces. |
2001010 | Access ID not found. |
2001014 | Register rate limit exceeded. |
2001015 | Register brute force limit exceeded. |
2001026 | Images sent to server are duplicated. |
401100 | You are not authorized to perform the action. |
4001000 | Input parameter violates a constraint. |
4001001 | The X-TV-Timestamp header is expired, need a newer one. |
4041000 | The image ID is not found in DB. |
4041001 | User not found. |
4091000 | The cus_user_id has been registered. |
4131000 | The input image size exceeds the allowed limit (15MB). |
4151000 | The provided image format is not supported (JPG/PNG). |
4291000 | The number of requests exceeded your throughput limit. |
5001000 | Some unexpected error occurs while processing the request. |
String codes
| Code | Description |
|---|---|
access_id_not_found | Access ID not found. |
access_denied_exception | You are not authorized to perform the action. |
invalid_parameter_exception | Input parameter violates a constraint. |
request_time_too_skewed | The X-TV-Timestamp header is expired, need a newer one. |
image_not_found_exception | The image ID is not found in DB. |
image_too_large_exception | The input image size exceeds the allowed limit (15MB). |
invalid_image_format_exception | The provided image format is not supported (JPG/PNG). |
duplicated_image | Images sent to server are duplicated. |
image_has_no_faces | Image sent to the server has no faces. |
request_not_found_exception | The request ID is not found when polling result by id. |
request_timeout_exception | Request takes too long to process. |
rate_limit_exception | The number of requests exceeded your throughput limit. |
internal_server_error | Some unexpected error occurs while processing the request. |
Next Steps
Start here
- Getting Started — run your first session in about 10 minutes with cURL
- Choosing an Integration Method — API, Mobile SDK or Web SDK
Open the session
- Create Transaction — open an eKYC transaction and mint the SDK token
Capture and upload
- Upload Image — the mandatory first call that turns bytes into an
image_id - Download Image — retrieve a previously uploaded image
ID card verification
- Detect ID Cards — locate and classify cards inside an image
- Verify ID Card Sanity — quality gate before any ID card processing
- Read ID Card Info — OCR, MRZ, QR and NFC field extraction
- Parse QR Info — parse a raw NID QR string into structured fields
- Detect ID Card Tampering — forgery and alteration detection
Face verification
- Detect Faces — face detection and embedding vectors
- Verify Face Sanity — quality gate before any face processing
- Verify Face Liveness — presentation-attack detection
- Compare Faces — 1:1 face matching between two images
Deduplication
- Index Faces — enrol a face into a collection
- Search Faces — 1:N search for duplicates and blocklist hits
- Delete Faces — remove indexed faces from a collection
Close the session
- Get Transaction — read a transaction's current status
- Verify Transaction Authentication Key — redeem the key and close the transaction
Reference
- Usage Recommendations — request reconciliation, image labels and card types
- Verdict Definitions — the full meaning of every sanity, tampering and matching verdict
- Error Codes — the full numeric and string error code reference