KYC tiers
A customer's kycLevel is a computed property — you don't set it directly, it's derived from their verification history.
| Tier | Promotion requirement |
|---|---|
none | (default) Customer record exists, no verifications |
basic | A verified row with method: "nik_format" (NIK is 16 digits and self-consistent) |
standard | A verified row with method: "ktp_capture" (anti-spoof + tamper passed) OR method: "nik_dukcapil" |
premium | standard requirements + a verified face_liveness + a verified face_match to an enrolled reference |
Promotion is automatic — every call to a verification endpoint re-evaluates the tier and writes a KycTierEvent row if it changed.
Read tier state
/api/identity/kyc/{customerId}curl https://api.quantumelixir.tech/identity/api/identity/kyc/cus_01HXY... \
-H "Authorization: Bearer $QE_API_KEY"{
"customer": {
"id": "cus_01HXY...",
"fullName": "Budi Santoso",
"kycLevel": "premium",
"kycLastVerifiedAt": "2026-05-24T08:18:01.122Z",
"kycExpiresAt": "2027-05-24T08:18:01.122Z"
},
"eligible": "premium",
"verifications": [
{ "id": "ver_01HXY1...", "method": "nik_format", "status": "verified", "score": 100, "createdAt": "2026-05-24T08:14:30Z" },
{ "id": "ver_01HXY2...", "method": "nik_dukcapil", "status": "verified", "score": 100, "createdAt": "2026-05-24T08:15:01Z" },
{ "id": "ver_01HXY3...", "method": "ktp_capture", "status": "verified", "score": 87, "createdAt": "2026-05-24T08:15:12Z" },
{ "id": "ver_01HXY4...", "method": "face_liveness","status": "verified", "score": 91, "createdAt": "2026-05-24T08:17:42Z" },
{ "id": "ver_01HXY5...", "method": "face_match", "status": "verified", "score": 92, "createdAt": "2026-05-24T08:17:58Z" }
],
"history": [
{ "id": "kte_01HXY...", "fromTier": "none", "toTier": "basic", "actorUserId": null, "reason": "automatic on nik_format verify", "createdAt": "2026-05-24T08:14:30Z" },
{ "id": "kte_01HXZ...", "fromTier": "basic", "toTier": "standard", "actorUserId": null, "reason": "automatic on nik_dukcapil verify", "createdAt": "2026-05-24T08:15:01Z" },
{ "id": "kte_01HXA...", "fromTier": "standard", "toTier": "premium", "actorUserId": null, "reason": "automatic on face_match verify", "createdAt": "2026-05-24T08:17:58Z" }
]
}Field reference
| Field | Type | Description |
|---|---|---|
customer.kycLevel | enum | Current tier (none · basic · standard · premium) |
customer.kycLastVerifiedAt | ISO-8601 | When the current tier was last reaffirmed |
customer.kycExpiresAt | ISO-8601 | When the tier auto-decays back to standard (typically 1 year for premium) |
eligible | enum | The tier the customer could be on given their verification history |
verifications | array | All verification rows for this customer, newest first |
history | array | KycTierEvent rows — every tier change |
If eligible is higher than kycLevel, an analyst (or system) once downgraded the customer. history tells you when and why.
Analyst override
/api/identity/kyc/{customerId}/upgradeSometimes a customer's identity is verified through a channel that's outside Quantum Elixir (in-branch visit, manual document review, regulator notice). An admin can upgrade them manually:
{
"toTier": "premium",
"reason": "Manually verified by branch officer (ticket #2026-1245). Original KTP + face seen."
}Constraints:
- You can only jump one tier above the customer's
eligible— e.g. ifeligible: "standard", you can upgrade topremiumbut not skip up frombasictopremium. reasonis required, minimum 20 characters, free-text. Stored verbatim on theKycTierEvent.- The override appears in
historywithactorUserId = <your-admin-id>so compliance can audit it.
Downgrades happen automatically (e.g. when kycExpiresAt lapses) or by admin action — there is no public API for forced downgrades; use the dashboard for that.
Expiry
| Tier | Default expiry | What happens on expiry |
|---|---|---|
basic | Never | — |
standard | 2 years | Downgrades to basic |
premium | 1 year | Downgrades to standard |
Configurable per org. Customers below expiry get a customer.kyc_updated webhook 30 days before, 7 days before, and on the day of downgrade.
Re-verifying without re-collecting biometrics
If a customer is about to expire, a single fresh face_match (live selfie against existing enrollment) is enough to reset kycLastVerifiedAt. You don't need to re-capture KTP or re-run Dukcapil unless the underlying biometric reference has changed.