Skip to main content

Liveness checks

POST /v1/liveness/check decides whether the frames you send show a live person in front of the camera rather than a photo, a screen or a replayed clip. The verdict is always the server's; on-device face detection in the SDKs only helps the user frame the shot.

Modes​

modeFramesWhen to use
passiveexactly one imageLowest friction. A single selfie scored by the anti-spoof models.
burst3 to 5 parts named image_1 … image_5A short capture sequence. Adds a temporal check across frames.
challenge2 to 10 parts named image_<n> plus a challenge_token; a blink challenge needs at least 3Strongest. The server picks an action the user must perform.

Challenge flow​

  1. Call POST /v1/liveness/challenges. The response carries a challenge_token, the action the user must perform (blink, turn_left or turn_right) and expires_at, 120 seconds later.
  2. Capture frames while the user performs the action, following the rules below.
  3. Send them to /v1/liveness/check with mode=challenge and the token. The action is bound to the token server side; a caller cannot choose it, which is what stops one pre-recorded clip from being replayed against the same action every time.

Capturing a challenge​

What the frames contain matters more than how many there are. These rules come from reviewing real captures, and each one separated a pass from a false rejection:

  • Start neutral. image_1 is the baseline: facing the camera, eyes open, before the user acts on the prompt.
  • Four to six frames over about three seconds, and stop after the head returns to centre. Do not end on the full profile: a frame turned past about 45 degrees scores badly on models built for frontal faces. Ask for a 30 to 45 degree turn and a return, not a held pose.
  • Upright and unmirrored. Apply the sensor rotation before encoding, or write the EXIF orientation, and never draw a mirrored preview into the frame. The server checks the turn direction against the sensor's view, so a mirrored frame turns the wrong way. Prompt "turn your head to your left" so the user does not mirror themselves either.
  • Blink needs at least three frames, open, closed and reopened, captured from the preview stream at 200 ms or faster. A shutter capture is too slow for a 100 ms blink. If you cannot capture that fast, ask the user to close their eyes for a second.
  • Capture fresh frames on every attempt. A capture that was already scored is refused whatever token it is sent with (see below).
  • Wait for a new camera frame before each capture. Grabbing a stalled preview twice produces two identical frames, which fail as duplicate_frames.

Both SDK capture widgets follow these rules: EkycLivenessWidget on Flutter and FaceCapture.captureSequence() in the JS SDK.

What the server does with the frames​

  • Every frame is decoded, face-detected and quality-gated first. The anti-spoof models then score the most frontal frames; a frame captured mid-turn is used for the action and continuity checks but does not decide the spoof verdict.
  • A frame stored sideways with no orientation tag is turned upright before anything looks at it. Do not rely on this; it costs a second detection.
  • A challenge frame that loses the face, at the deepest point of a turn or because someone walked through the shot, is dropped as long as two usable frames remain. In passive and burst, a bad frame fails the check.
  • A second face is a rejection (multiple_faces) only when it is comparable in size to the subject. A small face in the background is ignored.

Frames are single use​

Every frame is hashed before inference, and once a check is scored the decoded pixels of its frames are remembered as well. Frames that were already scored for your tenant are refused with a 400 frame_replayed error, and so is a request that contains the same frame twice. Re-encoding or resizing a scored frame does not get around this, and neither does sending the same capture again under a new challenge token. The window is:

Key modeWindow
live24 hours
test60 seconds

A capture that has passed through a liveness check once cannot pass again in that window, whatever the outcome of the first check was. This is a replay check, not a face deduplication: a fresh capture of the same person is fine. For repeated integration tests with the same image, use a test key and wait a minute between runs, or capture fresh frames.

Retrying a timed-out upload​

If your client times out after the server has scored the frames, resending them is refused as a replay, and the lc_id never reached you. Send an Idempotency-Key with every liveness upload and retry with the same key: the gateway returns the stored result. See Idempotency. The Flutter widget does this on its own.

Result​

{
"lc_id": "lc_01a0799179d576da…",
"is_live": true,
"score": 0.91,
"threshold": 0.4,
"reason": null,
"mode": "passive",
"face_bbox": { "x": 312, "y": 402, "w": 540, "h": 540 },
"face_quality": { "…": "…" },
"model_version": "…",
"request_id": "…",
"correlation_id": null
}
  • is_live is score >= threshold. The threshold is your tenant's anti-spoof threshold from the Decision thresholds page in the dashboard.
  • reason names why a check is not live. Frames the server refuses outright come back as errors instead (no_face, low_image_quality, multiple_faces). On a 200 you will see:
reasonMeaningWhat to do
likely_printed_photo, likely_screen_replayThe anti-spoof models scored the frames below the threshold.For a genuine user: more light, closer, facing the camera, then retry.
likely_deepfakeThe deepfake detector scored the frames below its threshold.For a genuine user: a plain background, no filters or beautification, then retry.
challenge_action_not_detectedThe requested action was not seen, or was performed in the other direction.Prompt relative to the user's own left and right; capture fresh frames and retry.
identity_changedThe face is not the same person across the frames.Recapture; keep hands and other people out of the shot.
frame_discontinuityLandmarks jumped further between frames than a live capture allows.Capture at a steadier cadence.
duplicate_framesTwo frames have identical pixels.Wait for a new camera frame before each capture.

These labels are precedence-based, not verified diagnoses. Show your users neutral guidance ("the liveness check did not pass, please try again") and keep the reason for your logs.

  • face_quality holds informational metrics about the detected face; its keys can change between model versions, so do not branch on them.
  • components (the per-model scores behind the verdict) is present in test mode, and in live mode only when ops enable liveness_show_components for your tenant.
  • lc_id is stable; GET /v1/liveness/checks/:id returns the result later for audit.

Inside a verification session​

When the request is made with a session token that is bound to a verification, the check becomes that verification's liveness step. The verification's policy then applies: it can require a particular mode (validation_error if you send another) and it caps attempts (verification_closed once they are exhausted).

Webhook​

A scored check publishes liveness.check.completed with the result body. See Webhooks.