Face liveness + match
Two endpoints, designed to work together but usable independently.
- Liveness — "is this a live person, not a replay/mask/photo/deepfake?"
- Match — "does this face match the reference we have for this customer?"
For continuous step-up authentication (the most common use case), use the single combined endpoint POST /api/identity/auth/step-up instead.
Face liveness
/api/identity/face/livenessRuns a 6-voter fusion over the bundle:
- Flash reflectance — does the captured frame show the screen's flash colors?
- Challenge response — did the user blink / turn left / smile when asked?
- Silent passive liveness — neural net trained on print/screen/mask attacks.
- Deepfake detector — temporal artifacts unique to GAN-generated faces.
- rPPG (remote photoplethysmography) — micro skin-color changes from heartbeat.
- Device attestation — iOS AppAttest or Android Play Integrity, if present.
The fused verdict is live, spoof, or unclear. Number of voters required to agree depends on per-org policy (typically 4-of-6 for high tier).
Request
{
"customerId": "cus_01HXY...",
"bundle": {
"version": "1.0",
"sdkVersion": "qe-identity-sdk-rn@0.1.0",
"platform": "rn-ios",
"frames": ["<base64>", "<base64>", ...],
"challenges": ["blink", "turn_left", "smile"],
"responses": [
{ "challenge": "blink", "satisfied": true },
{ "challenge": "turn_left", "satisfied": true },
{ "challenge": "smile", "satisfied": true }
],
"capturedAt": "2026-05-24T08:15:12.481Z",
"passiveLivenessScores": [0.95, 0.93, 0.94],
"flashChallengeNonce": "9f8e7d6c-...",
"flashSamples": [{ "tMs": 100, "meanR": 235, "meanG": 30, "meanB": 30 }],
"rppgSamples": [{ "tMs": 100, "meanR": 220, "meanG": 130, "meanB": 110 }],
"deviceAttestation": {
"platform": "iOS",
"token": "<AppAttest payload>"
},
"signature": "<HMAC>",
"deviceId": "<optional, registered native devices only>"
},
"hmacKey": "<optional, web only>"
}Response
{
"verification": { "id": "ver_01HXY...", "status": "verified", "score": 91 },
"captureBundle": { "id": "cb_01HXY...", "verdict": "passed", "assuranceTier": "high", "framesStored": 3 },
"antiSpoof": {
"fused": "live",
"assuranceTier": "high",
"passed": 5,
"present": 6,
"voters": [
{ "name": "flash_reflectance", "passed": true },
{ "name": "challenge_response", "passed": true },
{ "name": "passive_silent", "passed": true },
{ "name": "deepfake_detector", "passed": true },
{ "name": "rppg", "passed": true },
{ "name": "device_attestation", "passed": false, "reason": "no attestation token" }
]
}
}A successful liveness check produces a CaptureBundle you can match later.
Face match
/api/identity/face/matchCompares a probe (live selfie) against a reference. Three reference modes:
| Mode | Trigger | Use case |
|---|---|---|
| Active pool | Default (no referenceImage, referenceEmbedding, faceEnrollmentId provided) | Standard step-up — match against best of all enrolled refs |
| Specific enrollment | faceEnrollmentId provided | Forensic — match against one historical enrollment |
| Client-supplied | referenceImage or referenceEmbedding provided | BYO reference (e.g. customer's photo from your existing records) |
Request
customerIdstringRequiredcaptureBundleIdstringliveImagestring (base64)faceEnrollmentIdstringreferenceImagestring (base64)referenceEmbeddingnumber[] (length 512)thresholdnumberResponse
{
"verification": { "id": "ver_01HXY...", "status": "verified", "score": 92 },
"match": true,
"verdict": "match | grey_zone | no_match",
"similarity": 0.71,
"threshold": 0.45,
"greyZoneFloor": 0.38,
"referenceSource": "enrollment | pool | image | embedding",
"referenceEnrollmentId": "fe_01HXY...",
"probeSource": "sealed | direct | stub",
"probeStub": false,
"perReference": [
{ "id": "fe_01HXY...", "source": "ktp_extract", "similarity": 0.71 },
{ "id": "fe_01HXZ...", "source": "dukcapil", "similarity": 0.68 }
],
"promotedEnrolmentId": "fe_01HXZ..."
}Verdicts
| Verdict | Meaning |
|---|---|
match | Similarity ≥ threshold (default 0.45) — confident match |
grey_zone | Similarity between greyZoneFloor (default 0.38) and threshold — route to analyst |
no_match | Similarity < greyZoneFloor — reject |
Step-up promotion
When a strong match comes from a sealed step-up probe, we automatically create a new step_up_promotion enrollment for that customer (in promotedEnrolmentId). Over time the pool gets richer, the match gets faster, and you can retire older references.
Listing enrollments
/api/identity/face/enrollmentscurl "https://api.quantumelixir.tech/identity/api/identity/face/enrollments?customerId=cus_01HXY...&activeOnly=true" \
-H "Authorization: Bearer $QE_API_KEY"{
"rows": [
{
"id": "fe_01HXY...",
"customer": { "id": "cus_01HXY...", "fullName": "Budi Santoso" },
"source": "ktp_extract",
"active": true,
"createdAt": "2026-05-24T08:15:12.481Z",
"lastMatchedAt": "2026-05-24T11:42:08.122Z",
"matchCount": 12,
"lastSimilarity": 0.71
}
],
"nextCursor": null
}We never return embeddings via the API
The 512-float ArcFace embeddings stay server-side. You get metadata (source, score history, hash), never the vector itself. This is by design — you couldn't reuse a leaked embedding anyway, but it tightens the blast radius if a sandbox key leaks.