📘 Public beta · Endpoints are stable; OpenAPI specs and SDKs ship monthly. See changelog →
Products
Identity Platform
Face liveness + match

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

POST/api/identity/face/liveness
Auth · API keyScope · verifications:viewRate limit · 20/min

Runs a 6-voter fusion over the bundle:

  1. Flash reflectance — does the captured frame show the screen's flash colors?
  2. Challenge response — did the user blink / turn left / smile when asked?
  3. Silent passive liveness — neural net trained on print/screen/mask attacks.
  4. Deepfake detector — temporal artifacts unique to GAN-generated faces.
  5. rPPG (remote photoplethysmography) — micro skin-color changes from heartbeat.
  6. 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

POST/api/identity/face/match
Auth · API keyScope · verifications:viewRate limit · 60/min

Compares a probe (live selfie) against a reference. Three reference modes:

ModeTriggerUse case
Active poolDefault (no referenceImage, referenceEmbedding, faceEnrollmentId provided)Standard step-up — match against best of all enrolled refs
Specific enrollmentfaceEnrollmentId providedForensic — match against one historical enrollment
Client-suppliedreferenceImage or referenceEmbedding providedBYO reference (e.g. customer's photo from your existing records)

Request

customerIdstringRequired
CUID of the customer.
captureBundleIdstring
ID of a passed liveness bundle. Exactly one of this or liveImage required.
liveImagestring (base64)
Bypass liveness gate. Use only for non-step-up workflows.
faceEnrollmentIdstring
Match against a specific historical enrollment.
referenceImagestring (base64)
Client-supplied reference image.
referenceEmbeddingnumber[] (length 512)
Client-supplied ArcFace embedding.
thresholdnumber
Match threshold (0.0–1.0). Default 0.45.

Response

{
  "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

VerdictMeaning
matchSimilarity ≥ threshold (default 0.45) — confident match
grey_zoneSimilarity between greyZoneFloor (default 0.38) and threshold — route to analyst
no_matchSimilarity < 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

GET/api/identity/face/enrollments
Auth · API keyScope · verifications:view
curl "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.