# Review (manual review)

The **Review** section of the admin console handles verification sessions routed to manual review. Reviewers inspect documents and applicant data, decide approval or rejection, and leave internal comments.

> **Integrators (server-to-server):** the same queue is available on the Tenant API under `/tenant/v1/kyc/review-tasks` and `/tenant/v1/kyb/review-tasks` with scopes `reviews:*` / `kyb.reviews:*`. See [Review tasks (Tenant API)](/en/api/review).
>
> Admin console HTTP paths below are relative to the admin API (`/admin/v1/...`), not the Tenant API.

## Task queue [#task-queue]

The queue lists tasks (sessions) waiting for manual review or in progress. Each task shows the session identifier, queue arrival date, current status, and the assigned reviewer.

### Filters [#filters]

To manage the queue, the following filters are available:

- **By task status** — new (waiting for assignment), in progress, approved, rejected.
- **By assigned reviewer** — tasks for a specific reviewer or unassigned tasks.
- **By date** — task arrival date range.

### Queue statistics [#queue-statistics]

The section header shows summary metrics:

- Number of tasks waiting for assignment.
- Number of tasks in progress.
- Total number of tasks processed in the current period.

## Working on a task [#working-on-a-task]

When you open a task, you have access to the following information:

### Session and applicant data [#session-and-applicant-data]

- Session and applicant identifier.
- Data extracted from the document (full name, date of birth, document number — when OCR is available).
- External user identifier (external\_user\_id).

### Documents [#documents]

- Uploaded document and selfie images, viewable at full size.
- Automated check results: document authenticity, image quality, face match.

### Check history [#check-history]

- Chronological order of verification step execution.
- Automated check results (document check, liveness, face match, AML).

## Reviewer actions [#reviewer-actions]

### Task assignment [#task-assignment]

- **Assign to me** — the reviewer takes the task. The status changes to "In progress".
- **Reassign** — transfer the task to another reviewer (for example, when a reviewer with broader permissions is needed).

### Decision [#decision]

- **Approve** — verification passes. The session receives the `approved` status.
- **Reject** — verification fails. The session receives the `rejected` status. On rejection, you must:
  1. Pick a **rejection reason** from the structured list (`GET /admin/v1/review/reject-reasons`), for example `blurry_document`, `face_not_matching`, `suspected_fraud`, or `expired_document`.
  2. Optionally enter an **additional comment** with an explanation.

Canonical reject codes are the same as on the Tenant API (`blurry_document`, `unreadable_document`, `face_not_matching`, … — full list in [Review tasks](/en/api/review#stats-and-reject-reasons)).

### Changing task priority [#changing-task-priority]

Through `PATCH /admin/v1/review/tasks/{task_id}/priority`, a reviewer with the right permission can change the task priority (for example, urgent VIP cases). The action is logged as `review.priority_updated`.

### Delegating a task [#delegating-a-task]

Through `POST /admin/v1/review/tasks/{task_id}/delegate`, a reviewer can delegate the task to the OneKYC manual-review team. The current delegation status is returned by `GET /admin/v1/review/tasks/{task_id}/delegation-status`.

### Comments [#comments]

Reviewers can add an internal comment through `POST /admin/v1/review/tasks/{task_id}/comments`. Comments are visible to other reviewers and stored in the task history for audit and context handover.

### Accessing uploaded documents [#accessing-uploaded-documents]

To view a specific applicant document, use `GET /admin/v1/review/tasks/{task_id}/documents/{document_id}/url` — it returns a signed URL with a limited lifetime.

### Queue statistics [#queue-statistics-1]

Aggregate review-task metrics (queue size, in progress, processed for a period) are available through `GET /admin/v1/review/stats` (or `GET /admin/v1/review/queue` with scope `kyc` / `kyb`).

## Resubmit requests [#resubmit-requests]

Sometimes automated checks pass, but during manual review a document field turns out to be unreadable (glare, blur, cropped frame, wrong document, expired, etc.). In that case you can **ask the user to resubmit a specific step** without rejecting the whole session. This preserves conversion and saves the user from running through verification from scratch.

### Use case [#use-case]

1. The reviewer opens a task in the review queue and sees that the front-side document photo is blurry.
2. Instead of rejecting, the reviewer clicks **"Request resubmit"**, picks the step (for example, `document_capture`) and the reason code (`blurry`).
3. The session moves to **awaiting_resubmit**, and the selected step is reset.
4. The user receives a notification and reopens the flow — they go straight to the reset step with the reason explained.
5. After successful resubmission, the session returns to the review queue automatically.

### Reason codes (`reason_code`) [#reason-codes-reason_code]

The allowed reason codes are:

| Code | Description |
|-----|----------|
| `illegible` | Text is unreadable |
| `blurry` | Image is blurred |
| `cropped` | Document is cropped |
| `glare` | Glare obscures the data |
| `wrong_document` | Wrong document type uploaded |
| `expired_document` | Document has expired |
| `partial` | Document is not fully captured |

The same codes are used in webhook payloads and in the message shown to the user.

### Limits and TTL [#limits-and-ttl]

- **Up to 3 requests per session by default.** A fourth attempt returns `409 Conflict` with code `conflict`.
- After the supporting backend version and schema are deployed, an approved database change can set `flow_review_tasks.resubmit_limit_override` to 4–10 for one task. Record that change in the operations audit trail. `NULL` keeps the default. Existing resubmit rows remain intact; the limit counts all requests regardless of status.
- **TTL — 7 days.** After that, the request transitions to `expired`, and the session closes with the final status `rejected` (if no other channel completed it).
- **No more than one active request per (session, step) pair at a time.** A duplicate attempt returns `409 Conflict` with code `resubmit_already_pending`.
- **The session must be active for review resubmit.** `in_progress` sessions are accepted. A completed session can also be accepted when it is in the manual-review/resubmit path; the backend reopens it and resets the selected step atomically. Requests against pending, paused, failed, cancelled, or expired sessions return `409 Conflict` with code `session_not_in_progress`.
- **Only applicant-actionable step types can be resubmitted.** The current allowlist is `document`, `selfie`, `phone_otp`, `email_otp`, `kyb_company_input`, `kyb_business_document`, and `kyb_beneficiary_kyc`.

### Endpoints [#endpoints]

#### Create a resubmit request [#create-a-resubmit-request]

```http
POST /admin/v1/review/tasks/{task_id}/request-resubmit
Content-Type: application/json

{
  "step_id": "550e8400-e29b-41d4-a716-446655440000",
  "reason_code": "blurry",
  "client_message": "Please re-take the document in daylight without glare",
  "internal_notes": "Date of birth field is unreadable due to glare"
}
```

- `step_id` — UUID of the specific flow step to reset.
- `reason_code` — required, from the canonical list above.
- `client_message` (optional) — text shown to the user.
- `internal_notes` (optional) — internal note for other reviewers; not shown to the user.

Response (`200 OK`):

```json
{
  "data": {
    "request_id": "550e8400-e29b-41d4-a716-446655440099",
    "task_id": "550e8400-e29b-41d4-a716-446655440010",
    "status": "pending",
    "reason_code": "blurry",
    "expires_at": "2026-05-04T10:00:00Z"
  }
}
```

The action is logged in the audit trail (`review.decided` + `decision=request_resubmit` + `reason_code`).

#### List task resubmit requests [#list-task-resubmit-requests]

```http
GET /admin/v1/review/tasks/{task_id}/resubmits
```

Returns every resubmit request for the task (including fulfilled / expired / cancelled), sorted by creation date.

### Request lifecycle [#request-lifecycle]

| Status | Description |
|--------|----------|
| `pending` | Request created, sent to the user, waiting for resubmission |
| `fulfilled` | The user resubmitted the step — the task is back in the review queue |
| `expired` | 7 days passed without resubmission — the session closes with rejection |
| `cancelled` | Request cancelled by the admin / tenant before fulfilment |

The `pending → fulfilled` transition happens automatically after the user completes the requested step again.

### User notification delivery [#user-notification-delivery]

OneKYC delivers the resubmit notification through the available user channels:

1. **WebSocket push** — instant, when the user has the verification flow open.
2. **Email / SMS** — sent when the user needs a session-resume link outside the open verification page.

### Webhook events [#webhook-events]

When a request is created or fulfilled, OneKYC sends webhook events:

- **`verification.resubmit_requested`** — a reviewer requested resubmission. The payload includes `session_id`, `flow_id`, `applicant_id`, `step_id`, `step_type`, `reason_code`, `client_message`, `attempt_number`, `requested_by_id`, `requested_by_name`, `expires_at`.
- **`verification.resubmit_fulfilled`** — the user resubmitted. The payload includes `session_id`, `flow_id`, `applicant_id`, `step_id`, `reason_code`, `attempt_number`, `fulfilled_at`.

Full payload schemas are in [Webhooks API](/en/api/webhooks#manual-review-resubmit-events-verificationresubmit_).

## Outcome of processing [#outcome-of-processing]

After a decision is made:

- The session receives a final status (`approved` or `rejected`).
- The result is available through the Tenant API and is sent to configured [webhooks](/en/api/webhooks) (events `verification.approved` / `verification.declined`).
- The decision, rejection reason, and comments are recorded in the [audit log](/en/admin/audit).

## Review comment webhooks [#review-comment-webhooks]

New comments on KYC review tasks emit `verification.comment.created`; new comments on KYB review tasks emit `kyb.review.comment.created`. Comments created in the tenant dashboard, the platform review console, and the tenant API use the same contract. These events do not change the verification decision or complete the session.

Subscribe to the exact event type for the required scope. Existing `*` subscriptions also receive these events. Delivery uses the existing webhook signature, environment filtering, retries, and delivery logs. Events are delivered at least once; consumers must deduplicate using the event `id` or `idempotency_key`. Arrival order is not guaranteed.

Example event data for KYC:

```json
{
  "scope": "kyc",
  "task_id": "550e8400-e29b-41d4-a716-446655440010",
  "session_id": "550e8400-e29b-41d4-a716-446655440001",
  "flow_id": "550e8400-e29b-41d4-a716-446655440002",
  "external_id": "customer_123",
  "applicant_id": "550e8400-e29b-41d4-a716-446655440099",
  "comment": {
    "id": "550e8400-e29b-41d4-a716-446655440011",
    "content": "Please provide a clearer document photo.",
    "created_at": "2026-10-16T08:00:00.123456Z",
    "author_type": "admin_user"
  }
}
```

For KYB, `scope` is `kyb` and `business_id` replaces `applicant_id` when present. Author types are `admin_user`, `superadmin_user`, `api_key`, and `system`. Employee identifiers and names are not included in the webhook. All task comments are client-visible; decision `internal_notes` and resubmission `client_message` are separate fields and do not produce comment events. Only newly created comments generate these events; existing comments are not replayed.

Retrieve comments with `GET /tenant/v1/kyc/review-tasks/{task_id}/comments` (`reviews:read`) or `GET /tenant/v1/kyb/review-tasks/{task_id}/comments` (`kyb.reviews:read`). Creating a comment uses `POST` at the same path and the corresponding write scope. The successful creation response adds `comment_id` and `created_at`, matching the saved comment and webhook snapshot. Each successful POST creates a new comment; webhook event deduplication does not make repeated creation requests idempotent. After an uncertain request outcome, reconcile using the comments API before resubmitting.

The comment, webhook event, and outbox entry are committed together. Failure to persist the event rolls back comment creation. HTTP delivery happens asynchronously after commit, so a successful comment response is not proof of successful HTTP webhook delivery; use delivery logs.
