Verification
Standalone tools
One-shot utilities — AML, face search, signature verify/search/enroll, document extract, passive and active liveness — with full request/response JSON and no session attached.
The /v3/tools/* endpoints mirror the console's Manual Tools. They are stateless: none of them create or mutate a verification session. Each is tenant-scoped by your API key (x-api-key), and image arguments are base64 strings — raw or a data: URI.
Warning
A tool result is a lookup, not a verification. It carries no liveness, no face match, and no audit trail. Don't use document-extract or aml-check as a substitute for a real module in a compliance-bearing decision — use the tools for enrichment, triage and operator tooling.
Conventions
- Auth:
x-api-key: <tenant_api_key>(orX-API-Key). See Authentication. - Images: base64 (optionally a
data:URI), JPEG or PNG only, max 8 MB. Other formats (HEIC, WEBP, GIF, …) return415 unsupported_image_type; a missing/undecodable image returns400 <field>_required/400 invalid_<field>. - Rate limit: the tools share a per-tenant bucket of 120 requests/minute (
kyc_v3_tools). Over-limit requests get429. - Entitlement + billing: each chargeable tool is gated by your plan. A disabled feature returns
403 feature_not_enabled(with afeaturesarray); an empty balance returns402 insufficient_credits. A successful call debits one use. - Errors:
/v3tool errors return{ "detail": "<code>" }with the HTTP status carrying the machine signal. Branch on status + the stable code, not the message.
| Endpoint | Purpose | Key body fields | Chargeable |
|---|---|---|---|
POST /v3/tools/aml-check/ | Sanctions/PEP/adverse-media screening | full_name | first_name+last_name, date_of_birth?, nationality?, country?, document_number?, lists?, match_threshold? | ✅ |
POST /v3/tools/face-search/ | 1:N face search over enrolled identities | image_base64, threshold?, top_k? | ✅ |
POST /v3/tools/signature-verify/ | 1:1 signature comparison | reference_base64, probe_base64, threshold? | ✅ |
POST /v3/tools/signature-search/ | 1:N signature search (beta) | image_base64, threshold?, top_k? | ✅ |
POST /v3/tools/signature-enroll/ | Add a signature specimen to an identity | individual_id | external_user_id, image_base64, label? | — |
POST /v3/tools/document-extract/ | Identity-document OCR | image_base64, document_type? | ✅ |
POST /v3/tools/business-document/ | Business-document (KYB) classification + OCR | image_base64 | ✅ |
POST /v3/tools/face-verify/ | 1:1 face match | reference_base64, probe_base64 | ✅ |
POST /v3/tools/ip-check/ | VPN/proxy/Tor/hosting risk + geolocation for one IP | ip | ✅ |
POST /v3/tools/liveness/ | Passive liveness on a selfie (optionally + a short clip) | image_base64, video_base64?, check_sunglasses? | ✅ on a verdict |
POST /v3/tools/liveness/challenge/ | Mint a single-use active-liveness challenge | method (FLASHING | 3D_ACTION | GESTURE) | — |
POST /v3/tools/liveness/verify/ | Score frames captured for that challenge | challenge_id, frames / frames_base64, per-method metadata | ✅ on a verdict |
POST /v3/tools/lens/{service}/ | Lens OCR for one enabled service | image_base64 | metered only when LENS_<SERVICE> has an explicit rate |
Note
{service} on the Lens route is one of the Lens services enabled for your account — caf-front, caf-back, ont-front, ont-back, fdb-box, iptv, house-detection, speed-test, general, and the identity/insurance services. An unregistered name returns 404 invalid_lens_service with the allowed list; a service that isn't on your plan returns 403 lens_service_not_enabled. The upstream Lens payload is returned verbatim, so the response shape is owned by the service.
AML check
Ad-hoc watchlist screening for a name — the same engine as the AML_SCREENING module. A name is the minimum useful selector; every other field narrows the candidate set.
curl -X POST "$TF_BASE_URL/v3/tools/aml-check/" \
-H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
-d '{
"full_name": "Aarav Sharma",
"date_of_birth": "1990-04-12",
"nationality": "NP",
"lists": ["sanctions", "pep"],
"match_threshold": 0.7
}'const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/aml-check/`, {
method: "POST",
headers: {
"x-api-key": process.env.TF_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
full_name: "Aarav Sharma",
date_of_birth: "1990-04-12",
nationality: "NP",
lists: ["sanctions", "pep"],
match_threshold: 0.7,
}),
});
const screen = await res.json();
console.log(screen.risk_level, screen.matches.length);import requests
res = requests.post(
f"{BASE_URL}/v3/tools/aml-check/",
headers={"x-api-key": API_KEY},
json={
"full_name": "Aarav Sharma",
"date_of_birth": "1990-04-12",
"nationality": "NP",
"lists": ["sanctions", "pep"],
"match_threshold": 0.7,
},
timeout=30,
)
screen = res.json()
print(screen["risk_level"], len(screen["matches"])){
"risk_level": "medium",
"matches": [],
"possible_matches": [
{
"entity_id": "NK-9x2",
"name": "Aarav Sharma",
"schema": "Person",
"score": 0.63,
"match": false,
"lists": ["pep"],
"topics": ["role.pep"],
"datasets": ["everypolitician"],
"birth_date": "",
"countries": ["np"],
"aliases": [],
"nationalities": ["np"],
"positions": ["Member of Parliament"],
"gender": "male",
"addresses": [],
"id_numbers": [],
"url": ""
}
],
"informational_matches": [],
"lists_checked": ["pep", "sanctions"],
"provider": "watchlist",
"dataset": "default",
"algorithm": "logic-v1",
"threshold": 0.7
}risk_level is high / medium / low / clear. A query with no name returns 400 name_required; an unreachable engine returns 503 screening_unavailable. Full field reference, list topics, scoring tiers and the corroboration rule: AML screening.
Face search
Ranks your tenant's enrolled identities whose face matches a probe photo — duplicate-account detection. Read-only (nothing is flagged); it mirrors the automatic duplicate check.
The gallery it searches holds selfies captured live during a session and photos bulk-imported through identity import — document portraits and liveness stills stay out of it. If you imported an existing customer base, create each returning customer's session with vendor_data set to the id they were imported under: verifying without it spawns a second identity that the duplicate check then flags against their own imported record. See importing existing identities.
curl -X POST "$TF_BASE_URL/v3/tools/face-search/" \
-H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
-d '{"image_base64":"<jpg/png base64>","threshold":0.35,"top_k":12}'const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/face-search/`, {
method: "POST",
headers: {
"x-api-key": process.env.TF_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ image_base64: imageB64, threshold: 0.35, top_k: 12 }),
});
const { matches } = await res.json();
console.log(matches.map((m: any) => [m.external_user_id, m.score]));import requests
res = requests.post(
f"{BASE_URL}/v3/tools/face-search/",
headers={"x-api-key": API_KEY},
json={"image_base64": image_b64, "threshold": 0.35, "top_k": 12},
timeout=30,
)
for m in res.json()["matches"]:
print(m["external_user_id"], m["score"])image_base64stringbodyrequiredthresholdnumberbodytop_knumberbody{
"matches": [
{
"individual_id": "9f3a…",
"name": "Aarav Sharma",
"external_user_id": "user-123",
"status": "active",
"kyc_status": "clear",
"score": 0.71,
"thumb": "https://…/face.jpg"
}
],
"threshold": 0.35
}422 no_face_detected if there's no usable face in the probe.
Signature verify
Compares a probe signature to a reference — structural similarity of stroke geometry (not pen dynamics). Both inputs are cropped to just the signature ink first, so a full document photo or a video frame is matched on its ink, not the surroundings.
curl -X POST "$TF_BASE_URL/v3/tools/signature-verify/" \
-H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
-d '{"reference_base64":"<base64>","probe_base64":"<base64>","threshold":0.62}'const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/signature-verify/`, {
method: "POST",
headers: {
"x-api-key": process.env.TF_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
reference_base64: referenceB64,
probe_base64: probeB64,
threshold: 0.62,
}),
});
const out = await res.json();
console.log(out.verdict, out.score);import requests
res = requests.post(
f"{BASE_URL}/v3/tools/signature-verify/",
headers={"x-api-key": API_KEY},
json={"reference_base64": reference_b64, "probe_base64": probe_b64, "threshold": 0.62},
timeout=30,
)
out = res.json()
print(out["verdict"], out["score"])reference_base64stringbodyrequiredprobe_base64stringbodyrequiredthresholdnumberbody{
"score": 0.68,
"verdict": "match",
"matched": true,
"threshold": 0.62
}The verdict is derived against the requested threshold:
verdict | Condition |
|---|---|
match | score ≥ threshold |
review | score ≥ 0.75 × threshold |
mismatch | below the review floor |
Note
If an input can't be read as a signature you get a 422 — no_signature_detected when no stroke-shaped ink is found, or not_a_signature when a human face is detected (a photo of a person is not a signature). The tool never returns a falsely-confident match.
Signature search (beta)
Ranks your tenant's enrolled signature specimens against a probe — a recall aid for operators, not a verdict. The probe is cropped to the signature ink first, the same treatment enrollment applies, so both sides of the search are built from the same kind of crop.
curl -X POST "$TF_BASE_URL/v3/tools/signature-search/" \
-H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
-d '{"image_base64":"<png base64>","threshold":0.6,"top_k":12}'const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/signature-search/`, {
method: "POST",
headers: {
"x-api-key": process.env.TF_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ image_base64: imageB64, threshold: 0.6, top_k: 12 }),
});
const { matches, beta } = await res.json();
console.log(beta, matches.length);import requests
res = requests.post(
f"{BASE_URL}/v3/tools/signature-search/",
headers={"x-api-key": API_KEY},
json={"image_base64": image_b64, "threshold": 0.6, "top_k": 12},
timeout=30,
)
data = res.json()
print(data["beta"], len(data["matches"]))image_base64stringbodyrequiredthresholdnumberbodytop_knumberbody{
"matches": [
{
"individual_id": "9f3a…",
"name": "Aarav Sharma",
"external_user_id": "user-123",
"status": "active",
"score": 0.64,
"thumb": "https://…/signature.png"
}
],
"threshold": 0.6,
"beta": true
}422 no_signature_detected if the probe has no readable signature. Confirm any hit with the 1:1 signature verify.
Signature enroll
Attaches a reference signature to an identity so it becomes searchable by signature search. The input is cropped to the signature ink first, so a full video frame / document photo enrolls a clean specimen.
curl -X POST "$TF_BASE_URL/v3/tools/signature-enroll/" \
-H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
-d '{"external_user_id":"user-123","image_base64":"<png base64>","label":"account-opening"}'const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/signature-enroll/`, {
method: "POST",
headers: {
"x-api-key": process.env.TF_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
external_user_id: "user-123",
image_base64: imageB64,
label: "account-opening",
}),
});
const out = await res.json(); // 201
console.log(out.sample_id, out.individual_id);import requests
res = requests.post(
f"{BASE_URL}/v3/tools/signature-enroll/",
headers={"x-api-key": API_KEY},
json={
"external_user_id": "user-123",
"image_base64": image_b64,
"label": "account-opening",
},
timeout=30,
)
out = res.json() # 201
print(out["sample_id"], out["individual_id"])individual_idstringbodyexternal_user_id).external_user_idstringbodyimage_base64stringbodyrequiredlabelstringbodyaccount-opening), truncated to 120 chars.{
"sample_id": "c2b8…",
"individual_id": "9f3a…"
}On success returns 201. An unknown person returns 404 individual_not_found; an image with no readable signature returns 422 no_signature_detected.
Document extract
Identity-document OCR — the same read as ID_VERIFICATION, run standalone. It uses the platform document-OCR service, so it works on any account regardless of the tenant's Lens config.
Gated by the DOCUMENT_OCR feature (403 feature_not_enabled when it is off, 402 insufficient_credits when the balance can't cover the rate) and charged one DOCUMENT_OCR use only when the read returned fields or generic text — 422 ocr_no_fields, 503 ocr_not_configured and other failed reads are not charged.
curl -X POST "$TF_BASE_URL/v3/tools/document-extract/" \
-H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
-d '{"image_base64":"<base64>","document_type":"passport"}'const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/document-extract/`, {
method: "POST",
headers: {
"x-api-key": process.env.TF_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ image_base64: imageB64, document_type: "passport" }),
});
const doc = await res.json();
console.log(doc.detected_type, doc.fields);import requests
res = requests.post(
f"{BASE_URL}/v3/tools/document-extract/",
headers={"x-api-key": API_KEY},
json={"image_base64": image_b64, "document_type": "passport"},
timeout=30,
)
doc = res.json()
print(doc["detected_type"], doc["fields"])image_base64stringbodyrequireddocument_typestringbodyOptional type hint to recover documents auto-detect misses: passport, drivers_license, citizenship, pan, disability_id. Auto-detect covers most cases, and an unrecognised value falls back to it; disability_id runs a best-effort generic extraction (generic_extraction: true). A non-string value returns 400 invalid_document_type (not charged).
fieldsobjecttranslated_fieldsobjectdetected_typestringconfidencenumberis_document_validbooleanhas_portraitboolean{
"fields": {
"full_name": "AARAV SHARMA",
"document_number": "PA1234567",
"date_of_birth": "1990-04-12",
"expiry_date": "2032-01-10",
"nationality": "NP"
},
"translated_fields": {},
"detected_type": "passport",
"confidence": 0.97,
"is_document_valid": true,
"has_portrait": true
}Template-less documents (e.g. disability cards) run a generic extraction — auto-detect fields plus best-effort local OCR text, flagged with generic_extraction: true and a full_text string. Aadhaar cards, which the classifiers can only call "national id", are relabelled to aadhaar from OCR-text signals (display-level only). 503 ocr_not_configured if the OCR service isn't available; 422 ocr_no_fields if nothing readable was found.
Liveness
Three endpoints, one engine: the same models, verdict rules and pass floors as the Liveness module with its default configuration. Unlike a session they keep nothing — the selfie, clip and frames are scored in memory and discarded; the only stored row is an active challenge (its colour/action sequence and expiry), which anti-replay needs and which is deleted a day after it expires.
If you'd rather not build a capture UI at all, run liveness as a one-module hosted session instead — see Liveness via API for when to pick which.
Gate and billing. Each call needs the LIVENESS module and the method key (PASSIVE, PASSIVE_VIDEO, FLASHING, 3D_ACTION, GESTURE) on your plan; a 403 feature_not_enabled lists every key that's off. This is stricter than a session, which never drops liveness: a session whose method is switched off quietly runs PASSIVE instead, even when PASSIVE itself is off. The tools never substitute a method — a switched-off key, PASSIVE included, is a 403. A call is billed exactly like one liveness run in a session: under the method key when that method has its own rate, otherwise under LIVENESS. Only a returned verdict (approved, declined or review) is billed — capture failures (422), an unavailable engine (503), input errors and challenge issuing are free.
Response. Every verdict has the same shape:
{ "method": "PASSIVE", "status": "approved", "passed": true, "score": 91.37 }| Field | Meaning |
|---|---|
method | PASSIVE, PASSIVE_VIDEO, FLASHING, 3D_ACTION or GESTURE. |
status | approved, declined or review (the engine couldn't decide either way). |
passed | true only when status is approved. |
score | 0–100, the same scale as decision_v3.liveness_checks[].score. |
reason | Present when not approved — e.g. liveness_below_threshold, video_liveness_failed, still_clip_mismatch, spoof_detected, sequence_altered, timing_anomaly, weak_color_response, screen_replay_suspected, flash_liveness_low, action_not_completed, insufficient_motion, planar_motion_detected, action_liveness_failed, gesture_not_performed, no_face_detected, antispoof_unavailable, gesture_liveness_failed. |
video_verified | Passive with a clip only: false when the hosted video engine didn't answer, or the clip couldn't be decoded to check it against the still, and the still was scored instead. |
per_action_pass / per_gesture_pass | 3D_ACTION / GESTURE only: {item: bool} for each challenged item. |
Model names, raw anti-spoof probabilities and thresholds are never returned.
Passive liveness
curl -X POST "$TF_BASE_URL/v3/tools/liveness/" \
-H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
-d '{"image_base64":"<jpg/png base64>","check_sunglasses":true}'const res = await fetch(`${process.env.TF_BASE_URL}/v3/tools/liveness/`, {
method: "POST",
headers: { "x-api-key": process.env.TF_API_KEY!, "Content-Type": "application/json" },
// Add video_base64 (MP4 or WebM, ≤ 20 MB) to run PASSIVE_VIDEO.
body: JSON.stringify({ image_base64: selfieB64 }),
});
const out = await res.json();
if (res.status === 422) console.log("retake:", out.reason ?? out.detail);
else console.log(out.status, out.score);import requests
res = requests.post(
f"{BASE_URL}/v3/tools/liveness/",
headers={"x-api-key": API_KEY},
json={"image_base64": selfie_b64, "video_base64": clip_b64}, # clip optional
timeout=60,
)
print(res.status_code, res.json())image_base64stringbodyrequired422s below), clip or not. It is what gets scored when no clip is sent or the video engine is unavailable.video_base64stringbodyPASSIVE_VIDEO.check_sunglassesbooleanbodytrue.{ "method": "PASSIVE_VIDEO", "status": "declined", "passed": false, "score": 18.2,
"reason": "video_liveness_failed", "video_verified": true }With a clip, the verdict is the clip's — but only for the person in the still. A few frames from the clip are face-matched against image_base64; when the clip shows someone else (or no readable face at all) the result is declined with still_clip_mismatch, so an approved PASSIVE_VIDEO still is safe to hand to POST /v3/tools/face-verify/.
Errors: 422 quality_rejected with a reason you can show as a retake hint (too_blurry, face_too_small, poor_lighting, sunglasses_detected, face_occluded, face_not_frontal, low_quality, multiple_faces); 422 no_face_detected; 413 video_too_large; 415 unsupported_video_type; 503 liveness_engine_unavailable. None are billed.
Active liveness: challenge
Mint a challenge, run it in your own capture UI, then verify. A challenge is single-use, expires after 2 minutes (expires_at) and belongs to your tenant only.
curl -X POST "$TF_BASE_URL/v3/tools/liveness/challenge/" \
-H "x-api-key: $TF_API_KEY" -H "Content-Type: application/json" \
-d '{"method":"FLASHING"}'const ch = await fetch(`${process.env.TF_BASE_URL}/v3/tools/liveness/challenge/`, {
method: "POST",
headers: { "x-api-key": process.env.TF_API_KEY!, "Content-Type": "application/json" },
body: JSON.stringify({ method: "3D_ACTION" }),
}).then((r) => r.json());
// ch.actions, ch.prompts, ch.window_ms → drive your capture screench = requests.post(f"{BASE_URL}/v3/tools/liveness/challenge/",
headers={"x-api-key": API_KEY}, json={"method": "GESTURE"},
timeout=15).json()methodstringbodyrequiredFLASHING, 3D_ACTION or GESTURE. Anything else is 400 invalid_method.The 201 body carries the same fields the hosted flow's capture screens use:
{ "challenge_id": "0b6a2c1e-…", "method": "FLASHING",
"sequence": ["R", "W", "G", "B", "R", "G", "B"],
"frame_ms": 450, "lead_ms": 250, "n": 7, "expires_at": "2026-09-13T08:02:00Z" }{ "challenge_id": "…", "method": "3D_ACTION", "actions": ["turn_left", "look_up", "smile"],
"prompts": {"turn_left": "Turn your head to the left", "look_up": "Tilt your head up", "smile": "Smile"},
"window_ms": 4000, "mirror": true, "expires_at": "…" }{ "challenge_id": "…", "method": "GESTURE", "gestures": ["palm", "like", "peace"],
"prompts": {"palm": "Show your open palm", "like": "Give a thumbs up", "peace": "Show a peace sign (two fingers)"},
"required": 2, "window_ms": 4000, "expires_at": "…" }Capture contract (what the hosted UI does — the scorers are tuned to it):
- FLASHING — front camera, face filling the frame, screen brightness up. Fill the screen black for ~500 ms, then for each colour in
sequence(R#ff0000,G#00ff00,B#0000ff,W#ffffff): paint the whole screen that colour, waitlead_ms(so the camera exposes the new light), grab one frame and record its timestamp, then hold untilframe_mshas elapsed since the colour appeared. You end with exactlynframes.emitted_orderis the list of colours you actually painted, in order (normally equal tosequence; any difference declines assequence_altered).frame_timestampsare the capture times in milliseconds from any monotonic clock (e.g.performance.now()), one per frame and strictly increasing — out-of-order or mismatched timestamps decline astiming_anomaly. - 3D_ACTION — for each action: show its prompt (mirror the preview when
mirroris true), give ~700 ms to start moving, then grab frames at ~10 fps forwindow_ms. Label every frame with the action it was captured under inframe_actions. - GESTURE — same as 3D_ACTION with
promptsfor hand gestures (~900 ms lead, ~9 fps forwindow_ms), labelled inframe_gestures. At leastrequiredgestures must be recognised, with the face in view while each is performed — hand-only frames decline asno_face_detected.
Active liveness: verify
Accepts multipart/form-data — repeated frames files, metadata arrays as JSON strings, exactly what the hosted flow sends — or JSON with frames_base64. Frames are JPEG/PNG, at most 180 frames, 2 MB each and 32 MB total.
# FLASHING, multipart
curl -X POST "$TF_BASE_URL/v3/tools/liveness/verify/" \
-H "x-api-key: $TF_API_KEY" \
-F challenge_id=0b6a2c1e-… \
-F [email protected] -F [email protected] -F [email protected] -F [email protected] \
-F [email protected] -F [email protected] -F [email protected] \
-F 'emitted_order=["R","W","G","B","R","G","B"]' \
-F 'frame_timestamps=[812.4,1263.0,1714.8,2166.1,2617.5,3068.9,3520.2]'// 3D_ACTION, multipart (browser or Node 18+)
const fd = new FormData();
frames.forEach((blob, i) => fd.append("frames", blob, `f${i}.jpg`));
fd.append("challenge_id", ch.challenge_id);
fd.append("frame_actions", JSON.stringify(frameActions)); // one label per frame
const out = await fetch(`${process.env.TF_BASE_URL}/v3/tools/liveness/verify/`, {
method: "POST",
headers: { "x-api-key": process.env.TF_API_KEY! },
body: fd,
}).then((r) => r.json());
console.log(out.status, out.per_action_pass);# GESTURE, JSON
res = requests.post(
f"{BASE_URL}/v3/tools/liveness/verify/",
headers={"x-api-key": API_KEY},
json={
"challenge_id": ch["challenge_id"],
"frames_base64": frames_b64, # capture order
"frame_gestures": frame_labels, # same length as frames_base64
},
timeout=120,
)
print(res.status_code, res.json())challenge_idstringbodyrequiredframes / frames_base64file[] / string[]bodyrequiredframes files, or JSON base64 strings, in capture order.emitted_orderstring[]bodyframe_timestampsnumber[]bodyframe_actionsstring[]bodyframe_gesturesstring[]bodymethodstringbody400 challenge_kind_mismatch if it isn't the challenge's method.{ "method": "GESTURE", "status": "declined", "passed": false, "score": 33.3,
"reason": "gesture_not_performed",
"per_gesture_pass": { "palm": true, "like": false, "peace": false } }Single use. Request validation runs before the challenge is spent, so a 400/413/415 lets you fix the request and retry with the same challenge_id. Once it gets past validation the challenge is claimed atomically: a second verify — even a concurrent one — gets 409 replayed and is neither scored nor billed. A verdict or a 503 spends the challenge; mint a new one to retry.
| Status | detail | When |
|---|---|---|
| 400 | challenge_id_required, frames_required, invalid_frames_base64, emitted_order_required, invalid_emitted_order, invalid_frame_timestamps, frame_actions_required, frame_gestures_required, invalid_frame_actions, invalid_frame_gestures, invalid_method | Missing or malformed input. |
| 400 | challenge_kind_mismatch | Metadata for a different method (e.g. frame_actions for a FLASHING challenge). Body adds challenge_method. |
| 400 | frame_count_mismatch / frame_labels_mismatch | FLASHING frames ≠ n (body adds expected_frames); labels ≠ frames. |
| 404 | unknown_challenge | No such challenge for your tenant (another tenant's id reads the same). A challenge more than 24 hours past expires_at has been deleted and also reads as unknown — mint a new one. |
| 409 | replayed | Already verified. |
| 410 | expired | Past expires_at (for the first 24 hours after it; see 404). |
| 413 | too_many_frames / frame_too_large / frames_too_large | Over the frame caps (body carries the cap). |
| 415 | unsupported_image_type | A frame isn't JPEG/PNG. |
| 503 | liveness_engine_unavailable | No real liveness engine loaded. |
Error codes
| Status | detail code | Meaning |
|---|---|---|
| 400 | <field>_required / invalid_<field> | Missing or undecodable image / name. |
| 400 | invalid_method / challenge_kind_mismatch / frame_count_mismatch | Liveness: bad method, or frames that don't fit the challenge. |
| 402 | insufficient_credits | Tenant balance below the tool's rate. |
| 403 | feature_not_enabled | The tool isn't on your plan (see features). |
| 404 | individual_not_found / unknown_challenge | Signature enroll target / liveness challenge doesn't exist. |
| 409 | replayed | Liveness challenge already verified. |
| 410 | expired | Liveness challenge past expires_at. |
| 413 | video_too_large / too_many_frames / frame_too_large / frames_too_large | Liveness clip or frames over the caps. |
| 415 | unsupported_image_type / unsupported_video_type | Not JPEG/PNG (HEIC, WEBP, GIF, …); clip not MP4/WebM. |
| 422 | no_face_detected / quality_rejected / no_signature_detected / not_a_signature / ocr_no_fields | Nothing usable in the input. |
| 429 | — | Over the 120 req/min tools bucket. |
| 503 | screening_unavailable / ocr_not_configured / liveness_engine_unavailable | The backing engine isn't reachable/configured. |