Reports
Reading a liveness report, from verdict and risk score to per-check evidence
GET /liveness/sessions/:id returns the session document. Once the pipeline has run it is the full report: a verdict, a risk score, one entry per check, the forensic evidence, the face-match summary, the challenge outcomes, capture and client facts, timings and the artifact index. POST /liveness/analyze returns the same document inline. The report never embeds media; frames and heatmaps are referenced by artifact id.
Verdict and statuses
| Check status | Meaning | Effect on the verdict |
|---|---|---|
pass | The check ran and found nothing wrong | None, though the check may still contribute risk points |
fail | The check found a violation | fail when the check is required, risk points otherwise |
review | Inconclusive, or inside a review band | The verdict is at least review |
skipped | Not applicable to this session (no video, no reference, no challenge, no face in the first frame), or not_needed: a forensic or provenance check that was not run because the session had already failed on the other checks, which no forensic result can turn around | None |
unavailable | A provider was missing or failed | Governed by the profile's unavailablePolicy when the check is required |
error | The stage threw | Same as unavailable |
unmeasurable | The check ran, but the subject's images could not be measured reliably enough to decide (reason mesh_unavailable: a head angle on a face the platform could not read precisely). The check neither passes nor fails; no less reliable measurement decides in its place | The capture is retaken while attempts remain, fail (images_unmeasurable) once they are spent |
Decision rules, in order:
- Any required check with status
failgivesverdict: "fail";failureReasonnames the check and its reason (challenge.script: missing_step). Anunmeasurablecheck elsewhere does not change that. - Any required check
unavailableorerrorunderunavailablePolicy: "fail"givesfail. - Nothing failed but a check is
unmeasurable: while the session has an attempt left, it gives no verdict at all. The session goes tostatus: "retake"and the subject captures again on a fresh challenge (see Retakes below). On the last attempt it givesfailwithfailureReason: "images_unmeasurable"and a risk score of 100. reviewwhen a required check is unavailable under thereviewpolicy, when any check has statusreview, or whenriskScorereaches the profile's review threshold.- Otherwise
pass.
riskScore runs from 0 to 100 and accumulates the contributions of the checks that did not fail outright: forensic results in a review band, inconclusive checks, and the weaker client-side signals, which can raise risk but never fail a session on their own. A pass therefore means every required check passed or was skipped, nothing asked for review, and the accumulated hints stayed under the review threshold.
Report shape
The snippets below come from one report for a mobile-active-idmatch session, verdict pass.
{
"schemaVersion": 1, "sessionId": "5b1f0c1e-8d3a-4f6b-9c2e-7a1d2e3f4a5b", "organizationId": "0c9f3e2a-6b1d-4e8f-a2c3-1d4e5f6a7b8c",
"profile": "mobile-active-idmatch", "externalRef": "onboarding-2026-000482", "metadata": { "channel": "app" },
"status": "completed", "verdict": "pass", "riskScore": 0, "failureReason": null, "progress": null,
"attempts": { "number": 1, "max": 3, "previous": [] },
"createdAt": "2026-09-03T10:12:04.000Z", "uploadedAt": "2026-09-03T10:12:41.000Z", "completedAt": "2026-09-03T10:13:13.000Z"
}config (not shown) is the resolved profile the session ran with, so a report can always be interpreted against the rules that produced it.
Checks
checks is the list every verdict is computed from, in pipeline order. Each entry carries an id, a kind (the check family), a status, whether it was required, its durationMs, sometimes a score or a rating, a short reason code when it did not pass, and a details object specific to that check.
{
"checks": [
{ "id": "media.probe", "kind": "media", "status": "pass", "required": true, "durationMs": 1640,
"details": { "codec": "h264", "width": 1280, "height": 720, "fps": 30, "durationSec": 15.2, "transcoded": "remux" } },
{ "id": "forensics.video", "kind": "forensics", "status": "pass", "required": true, "rating": 0, "durationMs": 21430 },
{ "id": "face.match", "kind": "face", "status": "pass", "required": true, "score": 0.512, "durationMs": 1 }
]
}| Check | Kind | What it covers |
|---|---|---|
capture.integrity | capture | Client-reported capture facts, as hints only |
attestation.device | attestation | The attested capture: the uploaded bytes were bound to a session nonce by the device's hardware-backed app attestation. Required on mobile-attested, where a missing attestation fails not_requested and an Android claim (not verified yet) fails platform_unsupported; on every other profile those two are skipped. Authoritative over the report's attestation section, see below |
media.probe | media | The uploaded video decodes and matches what the client claimed |
challenge.script, challenge.timing | challenge | The session's script was answered, in order and within its window |
challenge.<prompt>#<index> | challenge | One entry per step of the script, verified on the frames and on the video. Some steps add a companion entry ending in .client, described below |
temporal.single_face, temporal.identity_continuity | temporal | One face, clearly detected, in every frame, and the same person throughout. The identity is judged on the uploaded stills and the video frames at their claimed instants; the frames extracted mid-motion (the neighbours of a claimed instant, the interior samples of the head-direction step) are listed in details.excluded with their similarity and do not decide |
temporal.frame_video_consistency, temporal.media_reuse | temporal | The frames belong to the video, and the media was not submitted before |
forensics.video, forensics.frames, forensics.reference, forensics.document | forensics | The third-party forensic analysis of each medium; forensics.document reads each side of the identity document for synthesis and manipulation, the worst side deciding |
document.portrait, document.mrz, document.expiry | document | On a profile whose reference comes from the identity document: a portrait found, the machine readable zone read with every check digit right, the document not expired |
face.match | face | 1:1 comparison with the reference photo |
provenance.watermark | provenance | Provenance marks on the analysed stills |
media.probe details also carry alignment: for each still, how far the video's timeline sits from the instant the capture page claimed for it (offsetMs, positive when the video shows that pose later), with medianOffsetMs and spreadMs over the stills. A genuine capture reads within a frame or two of 0. A large, constant offset means the recording and the page's clock drifted apart, which explains video comparisons failing on an honest capture. It is informative and decides nothing.
Checks whose id ends in .client carry clientClaimed: true: they describe what the capture page reported, contribute risk points at most, and never help a session pass. A profile runs only the stages it lists, and the list may carry informational entries beyond the table above, so parse checks generically on id, kind and status rather than assuming a fixed set.
The report's attestation section (null when the upload carried no attestation) is the record written at intake for the attested capture: the claim, the platform's own digests of the received bytes, the outcome as verified then, and the assertion counter, kept so the binding can be audited after the media is purged. It is not the check. attestation.device is authoritative: the analysis re-hashes the stored media against those digests and fails the check with digest_mismatch (naming the artifact in details.storedArtifact) when they no longer agree, and a required check fails a record the intake had only skipped. When the two disagree, decide on the check; the section says what the intake saw.
Verification summary
verification groups the checks by family in the shape shared with the other verification products: pass, skipped and unavailable render as ā
(with the reason), review, fail, error and unmeasurable as š.
{
"verification": {
"CAPTURE": [ { "status": "ā
", "check": "media.probe" } ],
"CHALLENGE": [ { "status": "ā
", "check": "challenge.script" } ],
"TEMPORAL": [ { "status": "ā
", "check": "temporal.single_face" } ],
"FORENSICS": [ { "status": "ā
", "check": "forensics.video" } ],
"FACE_MATCH": [ { "status": "ā
", "check": "face.match" } ]
}
}Forensic evidence
The forensics section carries the forensic stage's readiness in status, one row per call in requests, and one row per detector result in evidence: the medium, the frame tag, the detector, its label, its rating, and the artifact id of a heatmap when one was returned and retention.keepHeatmaps is on.
{
"forensics": {
"status": "configured",
"requests": [ { "media": "video", "durationMs": 20950 } ],
"evidence": [ { "media": "video", "frameTag": "capture.mp4", "source": "ā¦", "label": "ā¦", "rating": 0, "heatmapArtifactId": "e2a1c3d4-ā¦" } ]
}
}rating is an integer: 0 means nothing was found, and a higher value means a result that is rarer on genuine media. The profile thresholds decide at which rating a check asks for review or fails, and a forensic result alone never fails a session that passed every other check. Heatmaps are fetched with GET /liveness/sessions/:id/artifacts/:artifactId.
DOCUMENT sits between CAPTURE and CHALLENGE on profiles with the identity document, and is empty elsewhere.
Identity document
On a profile whose reference comes from the subject's identity document (mobile-document-idmatch), document says what was read on it; it is null on every other profile.
{
"document": {
"status": "pass", "kind": "id_card", "portrait": "found", "expired": false,
"sides": [ { "side": "front", "attempts": 1, "forced": false }, { "side": "back", "attempts": 2, "forced": false } ],
"mrz": {
"status": "valid", "format": "TD1", "documentCode": "I", "issuingState": "UTO",
"surname": "ERIKSSON", "givenNames": ["ANNA", "MARIA"], "documentNumber": "D23145890",
"nationality": "UTO", "birthDate": "1974-08-12", "sex": "F", "expiryDate": "2032-04-15", "optionalData": "",
"checks": [ { "field": "documentNumber", "valid": true }, { "field": "birthDate", "valid": true },
{ "field": "expiryDate", "valid": true }, { "field": "composite", "valid": true } ]
}
}
}mrz.status is valid when every check digit agrees, invalid when one does not (document.mrz is then review), unreadable when no zone was found after the retakes (review too) and unavailable when the server could not run its OCR. The fields are personal data: they stay in the report as long as the report does. sides counts the uploads of each side and says whether the photo was forced past the capture page's framing checks. document.expiry fails a document past its expiry date and is skipped when the zone gives none. The portrait cut out of the front is the session's reference artifact.
Face match and challenge outcomes
faceMatch carries status, similarity (the score of the most frontal frame, named in bestFrameTag), min and mean over every frame, and the threshold the profile applied. It is null when the profile has no face.match stage, and carries status: "skipped" without a similarity when no reference was provided. falseMatch ({ rate, bound, basis }) says what the similarity means: the share of pairs of different people that reach it with this engine, measured on 16.5 million pairs of public photos (LFW), a cautious estimate for an identity document portrait. Read it as "1 in 1/rate"; bound: true above the measured range, where it is an upper bound. Treat the similarity as a screening signal, not as a certified 1:1 match.
challenges lists the steps of the script, each with its index, kind, outcome and serverVerified: true; the measurements behind an outcome are in the details of the matching challenge.<prompt>#<index> check. A step whose outcome is unmeasurable names the frames that could not be read in unmeasured (still, video, baseline, neutral); on the head-direction step each target carries its own status (pass, fail or unmeasurable, the last with reason mesh_unavailable). The step is fail when any target fails on measured data, unmeasurable when none fails and one could not be measured. Older reports may show a target in review with reason five_point_pitch: that reading was retired on 2026-09-19 and no longer occurs. An outcome always comes from the server's own measurement of the media, never from what the capture page reported. The list is as long as the script the profile served, and an active script is longer than its prompt count, so read it by index rather than assuming a length.
Capture, client and timings
{
"capture": { "codec": "h264", "width": 1280, "height": 720, "fps": 30, "durationSec": 15.2, "transcoded": "remux", "frames": 5 },
"client": { "userAgent": "Mozilla/5.0 (iPhone; ā¦) Safari/604.1", "deviceClass": "phone", "browser": "Safari", "handoff": "qr",
"consent": { "version": "2026-09-1", "acceptedAt": "2026-09-03T10:12:25.000Z" },
"origin": { "origin": "https://verify.verifiables.com", "page": "hosted" } },
"timings": { "probeMs": 140, "transcodeMs": 620, "framesMs": 880, "faceMs": 1210, "totalMs": 32100 }
}client.origin says which page uploaded the capture, read by the server from the upload's Origin header: hosted for the Verifiables capture page, other for a page of your own (origin names it), none when no browser sent the upload (a native app, a server). It is informative and decides nothing; reports created before 2026-10-05 do not carry it.
transcoded is remux when the upload was already H.264 in a compatible container, reencode when it had to be converted, none without a video. timings also carries one entry per forensic call; those dominate totalMs, so expect tens of seconds, and more on the longer clip an active profile produces. The forensic analysis runs last, after every other check: a session those checks already failed completes without it (its forensic checks read skipped: not_needed and their timings 0), so a failure comes back much sooner than a pass.
Artifacts and retention
{
"artifacts": [
{ "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "kind": "video", "sha256": "9f2cā¦", "sizeBytes": 3812440, "meta": { "filename": "capture.mp4" }, "purged": false },
{ "id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e", "kind": "frame", "sha256": "5e7aā¦", "sizeBytes": 148220, "meta": { "tag": "frame-0" }, "purged": false }
],
"retention": { "artifactsPurgeAt": "2026-09-04T10:12:41.000Z", "artifactsPurgedAt": null }
}Artifact kinds are video (raw upload), video_mp4 (transcoded), frame, reference, heatmap and heatmap_video; hashes are truncated here. After artifactsPurgeAt the bytes are dropped, purged becomes true and the artifact endpoint returns 410. The rows stay, so the report remains self-describing.
Retakes
Some captures cannot be measured reliably: a face in the dark, too close to the camera, cut by the frame, or turned so far that its landmarks cannot be read. The platform does not fall back on a less reliable measurement to decide such a capture. It asks the subject to do it again instead.
- A retake is never a verdict and never terminal. When nothing failed and a check is
unmeasurable, the session moves tostatus: "retake"withverdict: null. The capture page tells the subject their images could not be analysed and offers to start again. No webhook fires, nothing is billed separately: one session includes its attempts. - A retake starts on a fresh challenge. New prompts, new targets, new nonces; nothing of the earlier script can be reused. The media of the retaken attempt are deleted at once (the reference photo is kept), and the session's 10 minute window restarts from the retake.
- Attempts are bounded. A session allows 3 capture attempts. If the third one cannot be measured either, the session completes with
verdict: "fail",failureReason: "images_unmeasurable"and a risk score of 100. It is a failure and not a review on purpose: images that cannot be measured are also what a screen, a mask or a deliberate blur produce, and making the measurement fail must not buy an attacker a human review. An honest subject who failed on lighting three times can simply be invited again, through a new session. - A failure stays a failure. If any check fails on data that was measured, the session fails as it always has, whatever else could not be measured.
The only terminal outcomes remain completed (with pass, review or fail), error and expired. If you poll, treat retake like challenged: the subject is still at work. A subject who abandons a retake lets the session expire, and the expired webhook tells you so.
Every document carries attempts: number is the attempt it describes (on a session in retake, the one about to start), max the bound, and previous one record per retaken attempt, oldest first. A record keeps what an audit needs once the media are gone: the attempt's checks, the digests of its discarded artifacts, and which checks could not be measured.
{
"attempts": {
"number": 2, "max": 3,
"previous": [
{ "attempt": 1, "outcome": "retake", "reason": "images_unmeasurable",
"unmeasurable": ["challenge.steer#5: mesh_unavailable"], "riskScore": 0,
"challengeIssuedAt": "2026-09-19T10:00:05.000Z", "capturedAt": "2026-09-19T10:01:10.000Z",
"completedAt": "2026-09-19T10:01:52.000Z",
"checks": [ "ā¦" ], "artifacts": [ { "kind": "video", "sha256": "9f2cā¦", "sizeBytes": 3812440, "purged": true } ] }
]
}
}The forensic analysis is not run on an attempt that ends in a retake, nor on the final attempt that fails images_unmeasurable (its checks read skipped: not_needed, as on any session that already failed).
Reading a review
A review verdict names its cause in the checks list. A forensic check in its review band means a detector scored the media above the profile's review threshold: look at forensics.evidence for the detector and the frame, open the heatmap, and decide with your own policy whether one such hit warrants a second capture or a manual check. A review on a temporal or challenge check means the evidence was inconclusive rather than wrong, and is usually worth a second capture.
A fail always names its cause in failureReason, as <check id>: <reason>, with one exception: images_unmeasurable, which names no single check because it is the outcome of three attempts the platform could not measure. Its final report shows the unmeasurable checks of the last attempt and two records in attempts.previous. Nothing in it says the subject is not genuine, only that their captures could not be analysed; whether to invite them again, on a new session in better conditions, is your call.
While processing
Before the report exists, GET /liveness/sessions/:id returns the same header fields and attempts with status: "processing" (or retake between two attempts), a progress object ({ "stage": "ā¦", "percent": 72 }) and null for every result section: checks, verification, forensics, faceMatch, challenges, capture, client, timings and artifacts. profile, externalRef, metadata, config and retention are present in that partial document too.