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
mode | Frames | When to use |
|---|---|---|
passive | exactly one image | Lowest friction. A single selfie scored by the anti-spoof models. |
burst | 3 to 5 parts named image_1 … image_5 | A short capture sequence. Adds a temporal check across frames. |
challenge | 2 to 10 parts named image_<n> plus a challenge_token; a blink challenge needs at least 3 | Strongest. The server picks an action the user must perform. |
Challenge flow
- Call
POST /v1/liveness/challenges. The response carries achallenge_token, theactionthe user must perform (blink,turn_leftorturn_right) andexpires_at, 120 seconds later. - Capture frames while the user performs the action, following the rules below.
- Send them to
/v1/liveness/checkwithmode=challengeand 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_1is 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
passiveandburst, 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 mode | Window |
|---|---|
| live | 24 hours |
| test | 60 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_liveisscore >= threshold. The threshold is your tenant's anti-spoof threshold from the Decision thresholds page in the dashboard.reasonnames why a check is not live. Frames the server refuses outright come back as errors instead (no_face,low_image_quality,multiple_faces). On a200you will see:
reason | Meaning | What to do |
|---|---|---|
likely_printed_photo, likely_screen_replay | The anti-spoof models scored the frames below the threshold. | For a genuine user: more light, closer, facing the camera, then retry. |
likely_deepfake | The deepfake detector scored the frames below its threshold. | For a genuine user: a plain background, no filters or beautification, then retry. |
challenge_action_not_detected | The 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_changed | The face is not the same person across the frames. | Recapture; keep hands and other people out of the shot. |
frame_discontinuity | Landmarks jumped further between frames than a live capture allows. | Capture at a steadier cadence. |
duplicate_frames | Two 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_qualityholds 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 enableliveness_show_componentsfor your tenant.lc_idis stable;GET /v1/liveness/checks/:idreturns 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.