Skip to main content

Client Actions (subject-facing)

These are the routes the subject — the person being verified — calls from their own browser or app. They are authenticated with an ephemeral token: short-lived, scoped to exactly one subject, and carrying only the capabilities you granted when you minted it.

Your backend never hands a bearer token to a subject. It mints an ephemeral token and passes that on. If you use the hosted verification UI, it calls these routes for you.

Pre-launch

Identiwise has not launched. This page describes the API as it is built today; the endpoints are not open to the public yet, and the hostnames in the examples do not resolve.

Base URL: https://{tenant}.identiwise.com/api/v1 Auth header: X-Api-Key: <ephemeral token>

What consumes a token use

An upload or re-upload submission consumes one use. Reads — details, messages, capabilities — never do. The use is reserved atomically before any storage or AI work, and it is not refunded if that work then fails: a spent use records that a submission was attempted. A token with max_uses: null is unlimited until it expires; an exhausted token is fully dead and every route denies it.

StatusMeaning
401The token is missing, invalid, expired, or exhausted.
403The token is valid but does not carry the capability this route needs — or this submission type's own attempt cap (max_license_attempts / max_selfie_attempts) is spent.
429The tenant's monthly API request quota is spent. The body carries the limit. This is a tenant-level limit, not a token one.

1. Read the token's capabilities

Endpoint: GET /ephemeral/capability

What this token is allowed to do, and how much of it is left. The hosted UI uses this to decide which steps to render.

{
"can_edit_details": true,
"view_subject_details": true,
"upload_type": "any",
"can_list_reupload": true,
"can_list_need_reply": true,
"can_return_capabilities": true,
"min_age": 18,
"require_face_match": false,
"max_uses": 3,
"use_count": 1,
"max_license_attempts": 2,
"license_use_count": 1,
"max_selfie_attempts": null,
"selfie_use_count": 0,
"workflow_steps": [
{ "step_name": "verify_license", "step_order": 1 },
{ "step_name": "check_selfie", "step_order": 2 }
]
}

A token minted without can_return_capabilities answers {"can_return_capabilities": false} and nothing else.

note

The response also echoes a few stored settings that nothing enforces today — the match thresholds among them. Build on the fields documented here and on Ephemeral Tokens; treat the rest as informational.


2. The subject's own details

The details on the subject record are the ground truth the OCR step scores a submitted document against, which is why letting the subject correct their own spelling before they photograph anything measurably improves the outcome.

Read — GET /subject/details

Requires the token capability view_subject_details, otherwise 403.

curl -s "https://acme.identiwise.com/api/v1/subject/details" \
-H "X-Api-Key: $EPHEMERAL_TOKEN"
{
"first_name": "Jane",
"middle_name": "",
"last_name": "Doe",
"license_number": "D1234567",
"license_state": "",
"license_issue_date": null,
"license_expiry_date": null,
"dob": "1994-03-11",
"addr_street": "123 Main St",
"addr_city": "Austin",
"addr_state": "",
"addr_zip": "",
"addr_country": "",
"email": "jane@example.com",
"photo_url": null
}

Write — POST /subject/details

Requires can_edit_details, otherwise 403. Send only the fields that changed. Accepted: first_name, last_name, dob (or date_of_birth), photo_url, middle_name, license_number, license_state, license_issue_date, license_expiry_date, addr_street, addr_city, addr_state, addr_zip, addr_country, email.

{ "first_name": "Jane", "addr_street": "123 Main St" }
{ "status": "updated" }

Both writes happen in one transaction. 404 if the subject was erased while the update was in flight (nothing is partly applied); 503 under database contention — retry.


3. Upload an image

Endpoint: POST /subject/upload?type=license|selfie|analyze|verify_age

multipart/form-data:

FieldTypeRequiredDescription
image_filefileYesThe image.
verification_idstringDependsYour id for this verification step. Required for selfie, analyze and verify_age; optional for license. Re-using it continues the same submission as a new attempt.
external_uidstringNoYour id for the person. Also usable as the submission's continuation key.
manualintNo1 skips the AI pipeline and files the submission straight into the admin review queue. It approves nothing, and it still consumes a token use.

The verification runs synchronously: the pipeline result is the response to this request. There are no webhooks — read later state through the admin routes.

Read the verdict from status in the body, not from the HTTP status code. A license or selfie upload whose pipeline ran answers 200; manual=1, analyze, verify_age and re-uploads answer 201.

type=license — document check

Document detection → face detection on the card (including the "ghost photo" internal-consistency check) → face match against an approved reference selfie, if the subject has one → OCR scored against the subject's stored details.

curl -s -X POST "https://acme.identiwise.com/api/v1/subject/upload?type=license" \
-H "X-Api-Key: $EPHEMERAL_TOKEN" \
-F "image_file=@/path/to/licence.jpg" \
-F "verification_id=order-88120-step-1"
statusMeaning
LICENSE_APPROVEDApproved automatically. Carries score, similarity and face_comparison.
LICENSE_PENDING_APPROVALParked for a human. Carries reason when there is one to state, and score when OCR ran.
NO_LICENSE_DETECTEDThe image does not contain a document the detector recognises.
NO_FACE_DETECTEDNo face was found on the document.
ID_NOT_MATCHEDThe face on the document did not match the approved reference selfie. Carries similarity.
SUBJECT_DATA_NOT_FOUNDThe subject has no stored details to score the document against.
ATTEMPT_SUPERSEDEDWhile this attempt was processing, an administrator decided the submission or a newer attempt replaced it. Whatever decided it stands; this attempt's result was discarded. The upload itself was still recorded.
MANUAL_REVIEW_REQUESTEDYou sent manual=1; the AI pipeline was skipped.

face_comparison is on every automatic approval and says what the approval is worth: matched — a face comparison against an approved reference selfie ran and passed on this attempt; none — a document-check approval, where no reference existed, so the approval asserts only that the document was detected and its text matches the subject's details. Mint the token with require_face_match: true if a document-check approval is not good enough for your flow; that licence then parks for review until one administrator approval anchors the subject.

similarity is a comparison figure recorded for the reviewer, never the verdict itself — the verdict is the model's own decision, not a threshold you configure.

similarity is not the same scale on both routes

On type=license it is a similarity (1 - distance): higher is a closer match. On type=selfie it is the model's raw distance: lower is a closer match. Do not threshold, chart or log the two through one comparison.

type=selfie — face check

{
"status": "SELFIE_APPROVED",
"similarity": 0.29,
"attempt": 1
}
statusMeaning
SELFIE_APPROVEDMatched the approved reference (the subject's approved document photo, or a selfie an administrator pinned as the basis).
SELFIE_PENDING_APPROVALParked for a human — including when there is no reference to compare against yet. Carries reason where there is one.
NO_FACE_DETECTEDNo face in the image.
MULTIPLE_FACESMore than one face in the image.
ID_NOT_MATCHEDDid not match the reference. Carries similarity.
ATTEMPT_SUPERSEDEDAs above.

reason — why a submission was parked

A *_PENDING_APPROVAL verdict carries reason when there is something to say. The value is one of a fixed set:

reasonMeaning
Multiple faces detected.More than two faces on the document.
Internal face mismatch on ID.The document's secondary photo does not match its primary one.
Reference image unavailable.The stored reference could not be retrieved.
Reference image has no detectable face.The stored reference is not usable as a face reference.
Reference face check unavailable.The face service failed while checking the reference.
Document detection unavailable.Document detection could not be reached.
Face detection unavailable.Face detection could not be reached.
Face comparison unavailable.Face comparison could not be reached.
Internal face comparison unavailable.The internal ghost-photo comparison could not be reached.
Text extraction unavailable.OCR could not be reached.
Verification exceeded its time budget.The run hit its wall-clock budget (150 seconds by default).
This token requires a face match for approval and no approved reference selfie is on file - an administrator must review this submission.The token's strict flow demands an identity anchor that does not exist yet. Nothing is down.
An outage is never a verdict about a person

Most of that list is infrastructure. When a dependency is unavailable the submission is parked with the reason, never rejected and never reported as NO_FACE_DETECTED or ID_NOT_MATCHED. Treat every one of these as undecided, awaiting a human.

type=analyze — age estimate

Returns an estimated age for the face in the image. It runs no identity comparison and decides nothing.

{
"status": "ANALYZED",
"selfie_id": "0198c3ff-0c18-7d62-9a04-5b1d9f2e6c30",
"attempt": 1,
"age_estimate": 28
}

type=verify_age — age gate

Compares the estimated age against the min_age the token was minted with.

{
"status": "AGE_VERIFICATION_DONE",
"selfie_id": "0198c3ff-0c18-7d62-9a04-5b1d9f2e6c30",
"attempt": 1,
"age_estimate": 24,
"age_verified": true
}

age_verified is false when the token carries no min_age, or when no age could be estimated. Both routes are an estimate from a photograph — treat them as a signal, not as proof of age.

Upload failures

StatusMeaning
400Missing or invalid image_file, missing/invalid ?type=, or verification_id missing for a type that requires it. Nothing is consumed.
403The token's upload_type forbids first uploads, or this type's attempt cap is spent. Nothing is consumed.
409The verification_id / external_uid names an already-approved submission for this subject. An approved submission is final; re-opening it is an administrator action (reject with require_reupload, then re-upload). Also answered when the submission was approved while this upload's storage write was in flight.
503Either the image could not be stored — nothing at all was recorded, retry the whole upload — or, on analyze / verify_age only, the AI service could not be reached. In that second case the image was stored and the row exists (named by selfie_id), the token use is spent, and status is ANALYSIS_UNAVAILABLE or AGE_VERIFICATION_UNAVAILABLE. Nothing is asserted about the person either way. Honour Retry-After.

analyze and verify_age answer 503 rather than parking for review because they have no review queue behind them — there is no reviewer to hand the question to.


4. Re-upload a rejected submission

Endpoint: POST /subject/reupload?type=license|selfie&id={uuid}

Available once an administrator rejects a submission with require_reupload. The subject finds the request in GET /subject/messages.

multipart/form-data: image_file (required), optional comment (posted to the message thread with the new image), optional manual.

curl -s -X POST "https://acme.identiwise.com/api/v1/subject/reupload?type=license&id=0198c3a1-2d44-7b90-8e21-11c4f0aa9b32" \
-H "X-Api-Key: $EPHEMERAL_TOKEN" \
-F "image_file=@/path/to/better-licence.jpg" \
-F "comment=Retaken without the glare."
{
"status": "license reupload complete",
"ai_result": {
"status": "LICENSE_APPROVED",
"score": 100,
"face_comparison": "matched"
}
}

The re-upload runs the same pipeline as a first upload, so the verdict lives in ai_result.status — the envelope's own status is a constant. A re-upload is a new attempt and consumes one token use. 409 if the submission was approved or un-flagged while the new image was being stored; 400 for a missing or malformed ?type= / ?id= or file.


5. Messages

The subject's side of the conversation with the reviewer.

GET /subject/messages

ParameterDescription
filter=reuploadOnly submissions awaiting a fresh image. Requires the can_list_reupload capability.
filter=need_replyOnly submissions awaiting a written reply. Requires can_list_need_reply.
license_id / selfie_idOne submission's thread. Give at most one.
{
"licenses": [
{
"license_id": "0198c3a1-2d44-7b90-8e21-11c4f0aa9b32",
"reupload_required": true,
"response_required": false,
"messages": [
{
"author_type": "admin",
"comment_text": "Please retake in better light.",
"created_at": "2026-09-04 09:31:07"
}
]
}
],
"selfies": []
}

POST /subject/messages

FieldTypeRequiredDescription
commentstringYesThe message text.
license_idstringOne ofThe licence submission this is about.
selfie_idstringOne ofThe selfie submission this is about.

Give exactly one of license_id / selfie_id; both or neither answers 400.

{ "status": "ok", "correspondence_id": "0198c44e-51aa-7ccf-9b70-2d0c8a6e1b93" }

6. Request erasure (Article 17)

Endpoint: POST /subject/request-deletion

The data subject asks for their record to be erased. This lodges the request in the tenant's privacy queue — it does not erase anything by itself; an administrator carries it out.

FieldTypeRequiredDescription
confirmationboolYesMust be true.
reasonstringNoFree text, maximum 4096 bytes.

One pending request per subject. A repeat while a request is still outstanding answers 200 with the id of the request that already exists, and records nothing new.

{
"status": "Deletion request recorded",
"subject_id": "0198c2f4-6b31-7c9a-8d55-2f9a1c7b40e1",
"request_id": "0198c31a-77b2-7f04-9c10-6d2ab3e1f550",
"reason": "I no longer use this service",
"confirmed": true
}

400 if confirmation is missing, or reason is not a string or exceeds 4096 bytes.

See Subject Management for the administrator's side of the same queue.