Working the queue
Claiming a case, deciding it, and what happens when two people reach for the same one
This is the part that needs a session, because a decision has to be recorded against a person.
The queue
GET /v1/console/reviews
GET /v1/console/reviews?holding=mineOpen cases, newest first. holding=mine is what that member is currently holding, which is the
"Mine" tab in our console.
Each row carries being_reviewed_by when a colleague has the case open right now, so you can grey it
out and send the agent to a different one. It is advisory: it stops two people wasting an afternoon
on one applicant, and it blocks nobody.
Claiming
GET /v1/console/reviews/policy
POST /v1/console/reviews/{id}/claim
DELETE /v1/console/reviews/{id}/claimMost organizations require a case to be held before it can be decided, and that is the default.
Check claim_required on the policy endpoint and render your interface accordingly, because deciding
without holding will be refused.
A claim lasts for claim_lease_minutes and then returns the case to the pool, so nothing is lost when
somebody closes their laptop. Claiming a case you already hold is fine and simply extends nothing;
releasing one you no longer hold is not an error either.
Deciding
POST /v1/console/reviews/{id}/decision
{ "decision": "approve", "notes": "…", "applicant_message": "…" }notes is internal and is never sent to anybody. applicant_message is the sentence the applicant
reads, and it is opt-in.
Approving or rejecting settles the case, moves the verification with it, and sends the applicant their outcome. The decision is stamped with the member who made it.
When somebody gets there first
Three refusals, and they mean different things:
| Code | What happened | What to do |
|---|---|---|
409 REVIEW_ALREADY_DECIDED | A colleague, our console, or your automatic policy settled it | Show the decision from error.detail. Do not retry |
403 REVIEW_NOT_HELD | Your organization requires a claim and this member is not holding it | Claim it, or send them to another case |
403 REVIEW_NOT_CLAIMABLE | Somebody else holds it, or it is already decided | Send them to another case |
The first is not an error condition to defend against, it is the normal result of two people working one queue. Exactly one decision is ever recorded, and the other person is told what the answer was. Render it as an outcome, not as a failure.
Handing a case up
A reviewer who cannot decide hands the case to a senior colleague, with a packet saying why.
POST /v1/console/reviews/{id}/escalation
{ "reason": "the address on page 2 does not match",
"recommendation": "reject",
"assessment": { "document": "mismatch" } }reason is required, because an escalation without one is a shrug. recommendation is the
reviewer's opinion and is explicitly not the outcome: nothing is decided by escalating.
assessment records what they did establish, keyed by check type, and we drop any check that did not
actually run on that verification, so a packet can never claim somebody verified a document on a
liveness-only flow.
You can only hand up a case you are holding, where your organization requires claiming. Escalating is
how a case leaves the working queue, so an unheld escalation would be a way around claiming
altogether. A case that is already decided or already escalated answers 403 REVIEW_NOT_ESCALATABLE.
Your integration hears about it as action.on_hold. The reason text, the recommendation and the
marks stay between colleagues and are never in the webhook.
Resolving one is the ordinary decision endpoint: a manager approves or rejects the escalated case as normal, and the escalation closes with it.
Handing it back undecided is different, and needs reviews.resolve_escalation:
POST /v1/console/reviews/{id}/escalation/return
{ "note": "check page 2 again" }The case returns to the queue with no verdict invented about the applicant, and the note is what the
reviewer who raised it reads when it reappears. The response carries raised_by_member_id so you can
tell that person yourself; we have no inbox on your side to write to.
Routing work to somebody
PUT /v1/console/reviews/{id}/assignment
{ "member_id": "…" } # or null, to return it to the shared poolNeeds reviews.assign, which is a different permission from deciding: handing work to somebody else
is not the same as taking work you are allowed to do.
Routing to a member who cannot decide is refused with 400 ASSIGNEE_CANNOT_DECIDE rather than
quietly accepted. Assigning a case to somebody with no button to press takes it off the shared pool
and parks it, which nobody notices until they ask why it has sat untouched for a week.
Telling people apart
presence_ttl_ms and presence_heartbeat_ms on the policy endpoint are the cadence our console uses.
If you want colleagues to see that one of your agents has a case open:
POST /v1/console/reviews/{id}/lock
DELETE /v1/console/reviews/{id}/lockRefresh on the heartbeat while the case is open, and release when it closes. If you stop refreshing, it lapses on its own. Presence is optional, and skipping it costs you nothing except that your agents are invisible to colleagues working in our console.