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.
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.
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
| Parameter | Type | Description |
|---|---|---|
pending | bool | 1 or true — return everything awaiting a decision. |
since | date | YYYY-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
| Field | What it tells the reviewer |
|---|---|
error_msg | The 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. |
score | The 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_score | The 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_details | The raw face-detection output the pipeline recorded, as JSON — the regions it found on the image. |
face_comparison | The 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. |
attempt | Which 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, approved | Decision state. review_status: needs_admin_review with admin_approval: null is the queue. |
reupload_required | Set 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
- Response
curl -s "https://acme.identiwise.com/api/v1/admin/review?externalUid=usr_55920" \
-H "Authorization: Bearer $IDENTIWISE_TOKEN"
{
"subject_id": "0198c2f4-6b31-7c9a-8d55-2f9a1c7b40e1",
"subject_external_uid": "usr_55920",
"licenses": [
{
"id": "0198c3a1-2d44-7b90-8e21-11c4f0aa9b32",
"attempt": 1,
"score": 50,
"admin_approval": "rejected",
"error_msg": "FLAGGED: Faces on ID do not appear to match.",
"face_comparison": null
},
{
"id": "0198c40b-9911-7a3d-b5cc-7ee2a0d41f77",
"attempt": 1,
"score": 100,
"admin_approval": null,
"approved": 1,
"face_comparison": "matched"
}
],
"selfies": [
{
"id": "0198c3ff-0c18-7d62-9a04-5b1d9f2e6c30",
"attempt": 1,
"admin_notes": "[AI Analysis] Age: 25",
"admin_approval": "approved"
}
]
}
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
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | license or selfie. |
id | string | Yes | The submission UUID. |
action | string | Yes | approve, reject, use or comment. |
notes | string | Depends | Free text. Required for comment; stored on the record for approve / reject. |
require_reupload | bool | No | reject only — asks the subject for a fresh image and unlocks POST /subject/reupload for that submission. |
require_subject_reply | bool | No | reject 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.
- Approve
- Reject + re-upload
- Use as basis selfie
- Comment
{
"type": "license",
"id": "0198c3a1-2d44-7b90-8e21-11c4f0aa9b32",
"action": "approve",
"notes": "Address matches the customer record."
}
{
"status": "SUCCESS",
"message": "License approved by admin"
}
{
"type": "selfie",
"id": "0198c3ff-0c18-7d62-9a04-5b1d9f2e6c30",
"action": "reject",
"notes": "Photo too dark - please retake in better light.",
"require_reupload": true
}
{
"status": "SUCCESS",
"message": "Selfie rejected by admin",
"require_reupload": true,
"require_subject_reply": false
}
The subject's hosted verification UI picks the request up from
GET /subject/messages and offers the re-upload step.
{
"type": "selfie",
"id": "0198c3ff-0c18-7d62-9a04-5b1d9f2e6c30",
"action": "use"
}
{
"status": "Basis selfie set",
"file": "tenant_storage/1/selfies/6f3a....jpg",
"action": "use"
}
use applies to a selfie only and pins it as the reference image the subject's future
document face-matches are compared against. Rejecting it on a license answers 400.
{
"type": "license",
"id": "0198c3a1-2d44-7b90-8e21-11c4f0aa9b32",
"action": "comment",
"notes": "Could you send a photo without the glare on the top-left corner?"
}
{
"status": "comment posted",
"correspondence_id": "0198c44e-51aa-7ccf-9b70-2d0c8a6e1b93"
}
The subject reads and answers it through GET/POST /subject/messages.
| Status | When |
|---|---|
200 | Approve, reject or use recorded. |
201 | Comment posted; the body carries correspondence_id. |
400 | Missing type / id / action, an unknown action, a malformed id, use on a licence, or comment with no notes. |
404 | No 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.
| Parameter | Type | Default | Description |
|---|---|---|---|
since | date | — | YYYY-MM-DD. Anything else answers 400. |
limit | int | 50 | Rows to return. |
offset | int | 0 | Pagination 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 keyThis 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.
| Parameter | Type | Description |
|---|---|---|
photo | string | original (the first image ever submitted for this record) or previous (the one before the latest re-upload). Omit for the current image. |
include | string | history returns the record's stored-object metadata as JSON instead of a redirect. |
- cURL
- 302 Response
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"
HTTP/1.1 302 Found
Location: https://s3.eu-central-4.ionoscloud.com/<bucket>/tenant_storage/1/licenses/6f3a....jpg?X-Amz-Algorithm=...
Follow it yourself, or hand the URL to a browser. It stops working after 15 minutes.
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.
| Status | When |
|---|---|
302 | Redirect to the pre-signed URL. |
404 | No 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. |
503 | Either 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.