The review workflow
Task management is in preview and available in the v1alpha2 API. Requests, responses, and activity-log payloads described in this guide may change without notice.
This page covers the four operations that take a review task from request to resolution: requesting a review, reassigning it, cancelling it, and completing it — including completing it on behalf of the person who made the decision.
Requesting a review
POST /v1alpha2/{parent}/manualTasks
CreateManualTask only accepts user-initiated task types. Today that means manual_task.task_type must be "REVIEW" — any other value is rejected. manual_task.assignee is required for a review task; there is no such thing as an unassigned review request.
Over REST, the request body is the ManualTask object itself; request-level fields such as on_behalf_of are query parameters. When an integration sets on_behalf_of, the named user is recorded as the task's creator_name — the task reads exactly as if that user had requested the review themselves.
curl -X POST "https://api.thirdfort.dev/client/api/v1alpha2/organizations/{org_id}/teams/{team_id}/checks/{check_id}/manualTasks?onBehalfOf=organizations%2F{org_id}%2Fusers%2F{requesting_user_id}" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Review sanctions match for John Smith",
"taskType": "REVIEW",
"status": "PENDING",
"assignee": "organizations/{org_id}/users/{user_id}"
}'
Assignee eligibility
The assignee named on create (and on reassignment — see below) must pass every one of these checks, in order. Each failure returns FAILED_PRECONDITION with a reason describing which check failed:
- The resource name is well-formed.
- The assignee belongs to the same organisation as the check.
- The assignee exists and the caller has permission to view them.
- The assignee is an active user (not
INACTIVE,INVITED, orDELETED). - The assignee is not soft-deleted.
- The assignee holds the review-task permission on the team.
- If four-eyes (
prevent-own-check-resolution) is enabled for the organisation or team, the assignee is not the check's creator, and not the user the check was created on behalf of.
Assigning a review to yourself on create is allowed: the same eligibility checks apply, and self-assignment powers the "review this myself" flow.
Four-eyes
When the prevent-own-check-resolution setting is enabled, the check's creator cannot be assigned to review their own check, and (per the delegation model — see Concepts and the task model) neither can the user the check was created on behalf of. This applies uniformly whether the caller is a portal user or an integration acting via on_behalf_of.
Reassigning a review
PATCH /v1alpha2/{manual_task.name}
UpdateManualTask with update_mask = ["assignee"] changes who a review task is assigned to. The new assignee is subject to the same eligibility rules as create, plus one more: you cannot reassign a review task to its current reviewer.
An assignment records who requested the review: for a portal user that is the caller, and for an integration it is the user named in on_behalf_of, recorded as the task's requester_name (with requested_time set alongside). An assignment by an integration without on_behalf_of records no requester.
The update must include the task's current etag (from your last read of the task) for optimistic concurrency. Over REST, the body is the ManualTask object and updateMask is a query parameter.
curl -X PATCH "https://api.thirdfort.dev/client/api/v1alpha2/organizations/{org_id}/teams/{team_id}/checks/{check_id}/manualTasks/{task_id}?updateMask=assignee" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Review check",
"status": "IN_PROGRESS",
"assignee": "organizations/{org_id}/users/{new_user_id}",
"etag": "<current-etag>"
}'
On a review task's first assignment, the server advances status from PENDING to IN_PROGRESS itself. Do not send status in the update mask for this transition — review-team roles do not hold the permission that would let a direct status update through, by design, so the server owns this specific advance.
Cancelling a review
POST /v1alpha2/{name}:cancel
CancelManualTask requires the task's current etag and accepts optional notes explaining why. A task already COMPLETED or CANCELLED cannot be cancelled again (FAILED_PRECONDITION).
curl -X POST "https://api.thirdfort.dev/client/api/v1alpha2/organizations/{org_id}/teams/{team_id}/checks/{check_id}/manualTasks/{task_id}:cancel" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"etag": "<current-etag>",
"notes": "Duplicate review — superseded by a fresh request"
}'
An assignee cannot cancel a review task assigned to them — that returns PERMISSION_DENIED. Cancelling is an action taken on a reviewer's queue, not something a reviewer does to their own outstanding work.
Completing on behalf of the assignee
POST /v1alpha2/{name}:complete
CompleteManualTask records the decision. The review completion payload carries decision (APPROVED or REJECTED) and optional notes; on_behalf_of — the mechanism this whole guide is built around — sits at the top level of the request, alongside the etag.
curl -X POST "https://api.thirdfort.dev/client/api/v1alpha2/organizations/{org_id}/teams/{team_id}/checks/{check_id}/manualTasks/{task_id}:complete" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"etag": "<current-etag>",
"review": {
"decision": "APPROVED",
"notes": "Sanctions match reviewed and dismissed as a false positive"
},
"onBehalfOf": "organizations/{org_id}/users/{user_id}"
}'
on_behalf_of is required when the caller is an integration, and must equal the task's current assignee — you can only complete a review on behalf of the person it is currently assigned to, not reassign and resolve in the same call.
Error catalogue
| Condition | Status |
|---|---|
A user (not an integration) sets on_behalf_of | INVALID_ARGUMENT |
on_behalf_of does not match the task's current assignee | FAILED_PRECONDITION |
The task is already in a terminal status (COMPLETED or CANCELLED) | FAILED_PRECONDITION |
Four-eyes is enabled and the effective user is the check's creator or on_behalf_of user | FAILED_PRECONDITION |
The supplied etag does not match the task's current etag | ABORTED |
An ABORTED response means the task changed since you read it — re-fetch the task, confirm it is still the assignee and status you expect, and retry with the fresh etag.
The atomic create-and-complete path
For the common case of an integration recording a review that was performed entirely in its own system — no separate "request" and "complete" round trip needed — CreateManualTask accepts a review_completion alongside the task, with manual_task.assignee set to the same user named in on_behalf_of:
Over REST, the body is the ManualTask object; review_completion and on_behalf_of are request-level fields, passed as query parameters:
curl -X POST "https://api.thirdfort.dev/client/api/v1alpha2/organizations/{org_id}/teams/{team_id}/checks/{check_id}/manualTasks?onBehalfOf=organizations%2F{org_id}%2Fusers%2F{user_id}&reviewCompletion.decision=APPROVED&reviewCompletion.notes=Reviewed%20in%20our%20internal%20case%20system%2C%20case%20%234471" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Review sanctions match for John Smith",
"taskType": "REVIEW",
"status": "PENDING",
"assignee": "organizations/{org_id}/users/{user_id}"
}'
This is one round trip from the caller's perspective, but it is not one transaction: the server performs the create and the complete as two separate updates against the same underlying check workflow. The create can succeed and the complete can subsequently fail — if that happens, the task is left IN_PROGRESS and assigned as requested, and can be resolved with an ordinary CompleteManualTask call rather than being retried atomically.
How completion appears
Once a review is completed — whether via the two-step path or the atomic one — three things are true on read:
completed_byon theManualTaskresource is the assignee, regardless of whether the actor that called the API was the assignee themselves or an integration completing on their behalf.- The parent check's
composite_statusmoves toREVIEW_APPROVEDorREVIEW_REJECTED. - A
notification.client.review_completed.v1notification is emitted (see Observing outcomes).
The actor that made the API call and the person recorded as having made the decision are both preserved — see Concepts and the task model for how the two are distinguished, and Observing outcomes for where each appears.