Hosted flow
The hosted flow is the capture experience your user sees: consent, document photos,
selfie, submit. It talks to the /v1/session/* endpoints with a
client token — never an API key. Most integrations let idcheck.now's hosted page
(or the SDK) drive these calls; they are documented here so you can build a
fully custom capture UI if you need to.
How the hosted flow works
POST /v1/verifications returns a client_token and a hosted
url of the form https://idcheck.now/verify/{id}?token={client_token}.
The hosted page sends the token as X-Client-Token on every session call. The token is
valid for 30 minutes and authorizes exactly one verification. All session endpoints are relative to
https://idcheck.now.
Get session state
To recover after a reload, GET /v1/session/state with the client token. It returns
the current status, any decision, and boolean flags describing what has been captured so far.
curl https://idcheck.now/v1/session/state -H "X-Client-Token: $CLIENT_TOKEN"const res = await fetch("https://idcheck.now/v1/session/state", {
headers: { "X-Client-Token": clientToken },
});
const state = await res.json(); // {status, has_front, has_back, has_selfie, ...}import requests
res = requests.get(
"https://idcheck.now/v1/session/state",
headers={"X-Client-Token": client_token},
)
state = res.json(){
"id": "b3f1c2a4-7d5e-4f6a-9c8b-1e2d3a4b5c6d",
"verificationId": "b3f1c2a4-7d5e-4f6a-9c8b-1e2d3a4b5c6d",
"status": "doc_front",
"decision": null,
"score": null,
"expires_at": "2026-07-27T14:03:11.204512",
"has_consent": true,
"has_front": true,
"has_back": false,
"has_selfie": false
}Capture consent
To record consent, POST /v1/session/consent. Both bipa (biometric
consent) and recording must be true — anything else returns
422 consent is required to proceed. The server stores the policy version, timestamp,
IP and user agent, then transitions the verification to consented.
Request body
| Field | Type | Description |
|---|---|---|
| biparequired | boolean | User accepted the biometric-information (BIPA) notice. |
| recordingrequired | boolean | User accepted image/video recording. |
| policy_versionrequired | string | Version string of the policy text shown, e.g. 2026-07-01. |
curl -X POST https://idcheck.now/v1/session/consent -H "X-Client-Token: $CLIENT_TOKEN" -H "Content-Type: application/json" -d '{"bipa": true, "recording": true, "policy_version": "2026-07-01"}'await fetch("https://idcheck.now/v1/session/consent", {
method: "POST",
headers: { "X-Client-Token": clientToken, "Content-Type": "application/json" },
body: JSON.stringify({ bipa: true, recording: true, policy_version: "2026-07-01" }),
});requests.post(
"https://idcheck.now/v1/session/consent",
headers={"X-Client-Token": client_token},
json={"bipa": True, "recording": True, "policy_version": "2026-07-01"},
){
"status": "consented"
}Submit a document image
To submit a driver's-license image, POST /v1/session/document once per side.
The front call stores the image and moves the state to doc_front.
The back call runs the barcode pipeline: the raw PDF417 payload is parsed server-side
(AAMVA CDS v01–v12), validated, and — when an image is supplied — independently re-decoded on
the server and compared against the client-supplied barcode. A mismatch is recorded as a tamper
signal.
Request body
| Field | Type | Description |
|---|---|---|
| siderequired | "front" | "back" | Which side of the document this payload describes. |
| image_b64optional | string (base64) | null | JPEG image bytes, base64. Required on front; on back it may be omitted only when barcode_raw is pasted manually (weaker assurance — the server re-decode is skipped and no_server_image is recorded). |
| barcode_rawoptional | string | null | Raw PDF417 barcode text as scanned (AAMVA CDS). Parsed and validated server-side; unparseable payloads return 422 barcode could not be parsed. |
| barcode_parsedoptional | object | null | Client-side parse of the barcode, stored for cross-comparison. |
curl -X POST https://idcheck.now/v1/session/document -H "X-Client-Token: $CLIENT_TOKEN" -H "Content-Type: application/json" -d '{"side": "back", "image_b64": "/9j/4AAQSkZJRg...", "barcode_raw": "@<RS>ANSI 636014090002DL00410278ZV03190008DLDAQD1234567..."}'const res = await fetch("https://idcheck.now/v1/session/document", {
method: "POST",
headers: { "X-Client-Token": clientToken, "Content-Type": "application/json" },
body: JSON.stringify({ side: "back", image_b64, barcode_raw, barcode_parsed }),
});
const { status, sha256, parsed, checks } = await res.json();requests.post(
"https://idcheck.now/v1/session/document",
headers={"X-Client-Token": client_token},
json={"side": "back", "image_b64": image_b64, "barcode_raw": barcode_raw},
){
"status": "doc_back",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"parsed": {"DAQ": "D1234567", "DCS": "DOE", "DAC": "JANE", "DBB": "06151990", "DBA": "04172029", "DAJ": "PA"},
"checks": {
"mandatory_elements": {"passed": true, "missing": []},
"not_expired": {"passed": true, "expiry": "2029-04-17"},
"server_client_barcode_match": {"passed": true}
}
}Submit a selfie
To submit the liveness capture, POST /v1/session/selfie with a short video and/or up
to 8 still frames. The server runs face matching (license portrait vs selfie, threshold 0.6),
passive liveness (threshold 0.5), and the 1:N duplicate-face search against previously verified
faces. At least one of video_b64 or frames_b64 is required
(422 otherwise).
Request body
| Field | Type | Description |
|---|---|---|
| video_b64optional | string (base64) | null | Short MP4 capture. Undecodable blobs degrade gracefully to frames-only. |
| frames_b64optional | string[] (base64) | Still JPEG frames; first 8 are stored. Used for face match and liveness. |
curl -X POST https://idcheck.now/v1/session/selfie -H "X-Client-Token: $CLIENT_TOKEN" -H "Content-Type: application/json" -d '{"frames_b64": ["/9j/4AAQSkZJRg...", "/9j/4B1SkZKms..."]}'const res = await fetch("https://idcheck.now/v1/session/selfie", {
method: "POST",
headers: { "X-Client-Token": clientToken, "Content-Type": "application/json" },
body: JSON.stringify({ frames_b64: frames }),
});
const { face_match_score, liveness_score } = await res.json();requests.post(
"https://idcheck.now/v1/session/selfie",
headers={"X-Client-Token": client_token},
json={"frames_b64": frames},
){
"status": "selfie",
"face_match_score": 0.91,
"liveness_score": 0.88
}Submit for decision
To finalize, POST /v1/session/submit (empty body) once at least the back document
has been processed. Submit is only allowed from status selfie or
doc_back — anything else returns 409 cannot submit from status
<status>. The server runs OFAC screening on the document identity, applies the
duplicate-face impact, computes the decision, dispatches the verification.decided
webhook, and enrolls the face embeddings for future 1:N checks — all synchronously.
curl -X POST https://idcheck.now/v1/session/submit -H "X-Client-Token: $CLIENT_TOKEN"const res = await fetch("https://idcheck.now/v1/session/submit", {
method: "POST", headers: { "X-Client-Token": clientToken },
});
const { decision, score, reasons } = await res.json();res = requests.post(
"https://idcheck.now/v1/session/submit",
headers={"X-Client-Token": client_token},
)
result = res.json() # {"status": "decided", "decision": ..., "score": ..., "reasons": [...]}{
"status": "decided",
"decision": "approve",
"score": 96.2,
"reasons": []
}State machine
| Transition | Trigger |
|---|---|
created → consented | POST /v1/session/consent accepted |
consented → doc_front | POST /v1/session/document with side=front |
doc_front → doc_back | POST /v1/session/document with side=back |
doc_back → selfie | POST /v1/session/selfie |
selfie|doc_back → submitted → processing → decided | POST /v1/session/submit (decision computed synchronously) |
The decision object
The decision engine sums weighted signals into a 0–100 score, then applies hard compliance and fraud overrides. The score is always visible; overrides can push the decision below what the score alone implies.
| Signal | Max points | How it's scored |
|---|---|---|
barcode_parse_ok | 10 | Pass/fail — the PDF417 payload parsed as AAMVA CDS. |
mandatory_elements | 15 | Pass/fail — all mandatory AAMVA data elements present. |
not_expired | 15 | Pass/fail — document expiry date is in the future. |
license_recompute | 10 | Pass/fail — license number recomputes correctly (FL/IL/WI; auto-pass elsewhere). |
server_client_barcode_match | 10 | Pass/fail — independent server re-decode equals the client barcode. Neutral (full points, noted) when server decode is unavailable. |
face_match | 25 | face_match_score × 25. Below 0.6 adds face_match_below_threshold. |
liveness | 15 | liveness_score × 15. Below 0.5 adds liveness_below_threshold. |
Overrides (applied after scoring; none can raise the decision):
- Degraded ML (face match or liveness unavailable) caps an
approveatreview(ml_degraded_capped_at_review). - OFAC
strongmatch →decline(ofac_match);possiblecaps atreview(ofac_possible_match); screening unavailable caps atreview(ofac_screen_unavailable) — a compliance check never fails open. - Duplicate face under a different identity →
decline(face_duplicate_different_identity); face seen before → caps atreview(face_seen_before); dedupe unavailable caps atreview.
Document checks (doc_checks)
Every check that ran is stored on the verification under checks.doc_checks (see
retrieve a verification). The complete check catalog:
| Check id | Meaning |
|---|---|
mandatory_elements | All mandatory AAMVA elements (name, DOB, expiry, license number, address…) are present. {passed, missing[]}. |
dob_parseable / expiry_parseable | The DBB / DBA date elements parsed to real dates. |
not_expired | Expiry date is today or later. |
dob_in_past | Date of birth is before today. |
date_ordering | DOB is before expiry. |
age_over_18 | Computed age is ≥ 18 (includes the computed age). |
license_recompute | License-number forensics for FL/IL/WI: Soundex surname prefix, encoded DOB/sex day-code, birth-year digits. Skipped (auto-pass) for other states. |
server_client_barcode_match | Server-side re-decode of the back image equals the client-supplied barcode. passed: null when the server decode was unavailable (degraded) or no image was supplied (manual paste). |
tamper_signal | Present when a mismatch was detected, e.g. {"barcode_mismatch": true}. |
no_server_image | Manual-paste path: barcode data is unverified client-supplied input (weaker assurance). |
ofac | OFAC screening result for the document identity: confidence, score, matched entity, dob_corroborated. See Screening. |
face_dedupe | 1:N duplicate-face result: matches[], impact (decline/review/null/unavailable) and reason (face_duplicate_different_identity, face_seen_before, returning_customer, below_threshold, no_matches). |
device | Device intelligence linked from /v1/signals/collect: visitor id, IP, user agent and bot signals (webdriver, headless_hints, no_languages, incognito). |
passed | Roll-up: true only if every validation check passed. |
All reason strings the engine can emit are listed in the error & reason reference.