Skip to main content

Admin Review & Decisions

Not every verification decides itself. A submission is parked for a human when the document cannot be read confidently, when the faces disagree, when the token's assurance flow demands a face match that has no reference yet — and, deliberately, when one of the AI services is unavailable. An infrastructure failure is never reported as a verdict about a person: it parks the submission with a stated reason and waits for a reviewer.

These routes are the review queue behind the admin console. They read and decide submissions; they are also how you find out what happened to a submission after the fact.

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.

Auth: Authorization: Bearer <system token>. Roles: admin or superuser to decide; the read-only view role can also read the queue, the submissions list, the history and the photo route.

There are no webhooks

Identiwise sends no callbacks and no push notifications of any kind. A verification result arrives in the HTTP response to the upload that produced it (see Client Actions); after that, read state by polling GET /admin/review or GET /admin/submissions.


1. The pending queue

Endpoint: GET /api/v1/admin/review?pending=1

ParameterTypeDescription
pendingbool1 or true — return everything awaiting a decision.
sincedateYYYY-MM-DD. Anything else answers 400.

Two lists come back, one per submission type. Each entry is the full submission row plus the subject's external_uid. The queue is newest-first and bounded at 1000 rows per list — narrow it with since rather than paging.

{
"licenses_pending": [
{
"id": "0198c3a1-2d44-7b90-8e21-11c4f0aa9b32",
"subject_id": "0198c2f4-6b31-7c9a-8d55-2f9a1c7b40e1",
"subject_external_uid": "usr_55920",
"verification_id": "order-88120-step-1",
"external_uid": "usr_55920",
"attempt": 2,
"score": 50,
"flag": 1,
"approved": 0,
"admin_approval": null,
"review_status": "needs_admin_review",
"error_msg": "FLAGGED: Faces on ID do not appear to match.",
"internal_match_score": 0.41,
"face_detection_details": "[{\"region\":{\"x\":40,\"y\":52,\"w\":88,\"h\":110}}]",
"face_comparison": null,
"admin_notes": null,
"reupload_required": 0,
"created_at": "2026-09-04 09:22:41"
}
],
"selfies_pending": []
}

Reading a review card

FieldWhat it tells the reviewer
error_msgThe reason line. Why this submission is here, in words — FLAGGED: Faces on ID do not appear to match., FLAGGED: Document detection unavailable (AI service error); manual review required., FLAGGED: Verification exceeded its time budget; manual review required., and so on. The admin console renders it on every card.
scoreThe OCR score: how much of the subject's stored detail was found in the document's text. Components are credited independently (licence number, first name, last name, date of birth, street); above 99 is an automated approval.
internal_match_scoreThe document's internal consistency — a secondary "ghost" photo on the card compared against the primary photo. Below 0.5 the submission is flagged. Null when the document carries only one face.
face_detection_detailsThe raw face-detection output the pipeline recorded, as JSON — the regions it found on the image.
face_comparisonThe assurance level of a standing pipeline approval: matched (a face comparison against an approved reference selfie ran and passed on this attempt) or none (a document-check approval — no reference existed, so the approval asserts only that the document reads correctly). null means there is no standing pipeline approval: the row is undecided, was reset by a new attempt, or was decided by a human.
attemptWhich attempt of this submission produced the state above. A retry continues the same submission and resets the previous attempt's AI-derived fields.
flag, review_status, admin_approval, approvedDecision state. review_status: needs_admin_review with admin_approval: null is the queue.
reupload_requiredSet by a rejection that asked the subject to try again.

2. One subject's full history

Endpoint: GET /api/v1/admin/review?subjectId={uuid} or: GET /api/v1/admin/review?externalUid={your-id}

Every licence and selfie the subject has submitted, decided or not. Supplying both parameters answers 400; add &pending=1 to narrow the same view to items still awaiting a decision.

curl -s "https://acme.identiwise.com/api/v1/admin/review?externalUid=usr_55920" \
-H "Authorization: Bearer $IDENTIWISE_TOKEN"

With ?subjectId=, a subject that has no submissions answers 200 with {"status":"No review data for the requested subject."}. With ?externalUid=, both an unknown identifier and a known subject with no submissions answer 404.

3. One submission

Endpoint: GET /api/v1/admin/review?type=license|selfie&id={uuid}

Or look one up by the identifier your own system supplied:

Endpoint: GET /api/v1/admin/review?type=license&identifier=verification&value=order-88120-step-1

identifier is verification (matches verification_id) or external (matches external_uid). Either form returns the single submission row; a miss answers 404.


4. Decide

Endpoint: POST /api/v1/admin/review

Body

FieldTypeRequiredDescription
typestringYeslicense or selfie.
idstringYesThe submission UUID.
actionstringYesapprove, reject, use or comment.
notesstringDependsFree text. Required for comment; stored on the record for approve / reject.
require_reuploadboolNoreject only — asks the subject for a fresh image and unlocks POST /subject/reupload for that submission.
require_subject_replyboolNoreject on a selfie only — the subject must reply in the message thread before re-uploading.

A human decision overrides the automated verdict. A manual approval or rejection is final for the submission: any pipeline run still in flight for that row discards its own result and answers ATTEMPT_SUPERSEDED. A manual decision also resets face_comparison to null — the marker describes an automated comparison, and after a human decides, the human is the anchor. Approving clears the flag and any outstanding reupload_required.

{
"type": "license",
"id": "0198c3a1-2d44-7b90-8e21-11c4f0aa9b32",
"action": "approve",
"notes": "Address matches the customer record."
}
{
"status": "SUCCESS",
"message": "License approved by admin"
}
StatusWhen
200Approve, reject or use recorded.
201Comment posted; the body carries correspondence_id.
400Missing type / id / action, an unknown action, a malformed id, use on a licence, or comment with no notes.
404No such submission in this tenant.

Approvals, rejections and use all write an audit-trail entry naming the actor, the submission and the outcome.


5. List submissions

Endpoint: GET /api/v1/admin/submissions

Licences and selfies in one time-ordered feed — the reporting view, rather than the queue.

ParameterTypeDefaultDescription
sincedateYYYY-MM-DD. Anything else answers 400.
limitint50Rows to return.
offsetint0Pagination offset.
[
{
"submission_id": "license-0198c3a1-2d44-7b90-8e21-11c4f0aa9b32",
"row_id": "0198c3a1-2d44-7b90-8e21-11c4f0aa9b32",
"submission_type": "license",
"subject_id": "0198c2f4-6b31-7c9a-8d55-2f9a1c7b40e1",
"admin_approval": null,
"review_status": "needs_admin_review",
"created_at": "2026-09-04 09:22:41",
"has_file": 1,
"score": 50,
"flag": 1,
"external_uid": "usr_55920",
"verification_id": "order-88120-step-1"
}
]
has_file, never the object key

This listing tells you whether an image is stored (has_file), never where. The object key it used to return handed out a durable pointer to an identity document, in bulk, through a door that records nothing. To reach an image, call the photo route below — it records the access and returns a link that expires.


6. One submission's timeline

Endpoint: GET /api/v1/admin/submissions/{id}/history?type=license|selfie

The submission's current decision state plus a merged, time-ordered timeline of its creation, its decision and every message exchanged about it.

{
"submission_id": "0198c3a1-2d44-7b90-8e21-11c4f0aa9b32",
"submission_type": "license",
"subject_id": "0198c2f4-6b31-7c9a-8d55-2f9a1c7b40e1",
"verification_id": "order-88120-step-1",
"admin_approval": "rejected",
"review_status": "needs_admin_review",
"flag": 1,
"admin_notes": "Photo too dark.",
"error_msg": "FLAGGED: Faces on ID do not appear to match.",
"timeline": [
{ "event_type": "creation", "timestamp": "2026-09-04 09:22:41", "details": "License submission created" },
{ "event_type": "rejection", "timestamp": "2026-09-04 09:22:41", "details": "Admin rejected license" },
{ "event_type": "comment", "timestamp": "2026-09-04 09:31:07", "author_type": "admin", "text": "Please retake in better light." }
]
}

type is required; omitting it answers 400, and a submission that is not this tenant's answers 404.


7. View the image

Endpoint: GET /api/v1/admin/photos/{license|selfie}/{id}

Stored images are private. They are never served from a public URL and are never returned inline. This route answers 302 with a pre-signed URL valid for 15 minutes.

ParameterTypeDescription
photostringoriginal (the first image ever submitted for this record) or previous (the one before the latest re-upload). Omit for the current image.
includestringhistory returns the record's stored-object metadata as JSON instead of a redirect.
curl -s -D - -o /dev/null \
"https://acme.identiwise.com/api/v1/admin/photos/license/0198c3a1-2d44-7b90-8e21-11c4f0aa9b32" \
-H "Authorization: Bearer $IDENTIWISE_TOKEN"
Every view is recorded, and recording comes first

Issuing the link writes an append-only audit entry — actor, tenant, submission, subject, object key, and which variant was asked for — before the link is handed over. If that entry cannot be written, no link is issued and the call answers 503. An identity document is never accessed untraced. The entry records the object key, never the signed URL.

StatusWhen
302Redirect to the pre-signed URL.
404No such submission in this tenant, no history for the variant you asked for, or the record holds no image at all (an erasure clears it). This means the image is genuinely not there.
503Either the link could not be signed — object storage is misconfigured or unavailable — or the access could not be recorded. Both are retryable; honour Retry-After.

That 404 / 503 split is deliberate. Signing a URL performs no I/O, so a signing failure can never mean "the object is absent" — and telling a reviewer that a real person's identity document does not exist, when in fact the store is down, is the wrong answer.