# Review queue

The review queue is where approval requests wait for a person to approve or deny them. These endpoints list the queue, retrieve one request, claim it while you look at it, and record your decision.

## List the review queue

`GET /api/v1/requests`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `request.read`

Returns the list of approval requests that reached human review: pending, decided by a person, or expired while waiting. Approval requests the pipeline decided automatically are not included. They stay in the audit log and can still be retrieved by id. Requests are returned newest first.

Pass `agent_slug` to list one agent's requests instead. That list includes the requests the pipeline decided automatically.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | How many requests to return. Defaults to 50, at most 200. |
| `agent_slug` | query | string | no | Show one agent's requests instead of the queue, including the ones the pipeline decided. |
| `offset` | query | integer | no | Number of visible matching requests to skip before returning this page. |
| `q` | query | string | no | Case-insensitive literal substring of the tool or instance name. |
| `status` | query | string | no | Return only requests with this outcome. |
| `scope` | query | string | no | Which layer of the queue to return, from the caller's standpoint. `notified` is what the caller was told about: requests a notification addressed to them, or a colleague handed them. `decidable` is what the caller may decide outright: requests routed to them directly, through a team, or by being added, plus unrouted requests where any reviewer may take them. `visible` (the default) is everything the caller may see, whoever decides it. Each layer contains the one before it. Ignored for API keys, which have no membership. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The requests, newest first |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | You cannot read requests |

Response body (200):

- `requests` · array<ApprovalRequest> · required

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/requests" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "requests": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "agent_slug": "support-agent",
      "agent_instance_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "agent_name": "support-agent",
      "agent_instance_name": "ci-runner-07",
      "tool": "issue_refund",
      "server": "stripe",
      "arguments": {
        "amount": 4900,
        "reason": "duplicate_charge"
      },
      "agent_reasoning": "Refunding the duplicate charge for jane@northwind.com.",
      "context": {
        "run_id": "4821",
        "framework": "claude-code"
      },
      "status": "approved",
      "deadline_at": "2026-09-09T12:00:00Z",
      "created_at": "2026-09-08T12:00:00Z",
      "decision": {
        "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "request_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "status": "approved",
        "note": "OK, but flag this account for review.",
        "channel": "web",
        "reviewer_display_name": "Chris",
        "reviewer_email": "reviewer@example.com",
        "authentication": {
          "method": "local_password",
          "assurance": "strong",
          "authenticated_at": "2026-09-08T12:02:11Z",
          "session_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "credential_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
        },
        "idempotency_key": "decision-7ab8c8ec",
        "decided_at": "2026-09-08T12:02:11Z"
      },
      "updated_at": "2026-09-08T12:05:00Z",
      "claim": {
        "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "display_name": "Chris",
        "claimed_at": "2026-09-08T12:01:30Z",
        "expires_at": "2026-09-08T12:16:30Z"
      },
      "presentation": {
        "source": "built_in",
        "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "title": "Edit {{/file_path}}",
        "fields": [
          {
            "path": "/body",
            "label": "Body",
            "kind": "text",
            "language": "python",
            "before": "/old_string",
            "after": "/new_string",
            "item": {},
            "role": "primary"
          }
        ]
      }
    }
  ]
}
```

## Retrieve a request

`GET /api/v1/requests/{id}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `request.read`

Returns one approval request. The response includes its current status, its decision once there is one, and any claim a reviewer holds on it. Pass `wait` to hold the response until the request is decided.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The request's id. |
| `wait` | query | string | no | How long to hold the response for a decision, as a duration such as `30s`. Capped at 30 seconds. Omit to return immediately. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | QueueApprovalRequest | The request in its current state |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | You cannot read requests |
| 404 | ErrorResponse | No such request |

Response body (200):

- `id` · uuid · required
- `organization_id` · uuid · required
- `agent_slug` · string · required: The slug of the agent that made the request.
- `agent_instance_id` · uuid · required: The running copy of the agent that made the request.
- `agent_name` · string · required: The agent's name when the request was made.
- `agent_instance_name` · string · required: The instance's name when the request was made.
- `tool` · string · required: The tool the agent wants to call, as the server that defines it names it.
- `server` · string: The MCP server that defines the tool, as the adapter reported it. Absent for a runtime's built-in tools.
- `arguments` · any · required: The exact arguments the tool will run with if approved.
- `agent_reasoning` · string: The agent's own explanation, if it gave one.
- `context` · any: Where the call comes from, as recorded by the adapter.
- `status` · enum · required: `pending` while waiting for a decision, then one of the four final states. `cancelled` means the requesting instance withdrew it; treat it as a denial. One of `pending`, `approved`, `denied`, `expired`, `cancelled`.
- `deadline_at` · date-time · required: When the request expires if nobody has decided: `created_at` plus the timeout.
- `created_at` · date-time · required
- `decision` · Decision: The outcome of an approval request and who produced it.
  - `id` · uuid · required
  - `request_id` · uuid · required
  - `organization_id` · uuid · required
  - `membership_id` · uuid: The member who decided. Absent when the pipeline decided, the request expired, or the agent cancelled it.
  - `status` · enum · required One of `approved`, `denied`, `expired`, `cancelled`.
  - `note` · string: A note the reviewer left for the agent, if any.
  - `channel` · string · required: Where the decision was made, as reported by the client, for example `web` or `slack`. `pipeline` when the pipeline decided, `system` when the request expired, `aap` when the agent cancelled it.
  - `reviewer_display_name` · string: The reviewer's name, when a person decided.
  - `reviewer_email` · email: The reviewer's email, when a person decided.
  - `authentication` · AuthContext · required: How a principal was signed in when it acted.
    - `method` · string · required: How the principal signed in, for example `local_password`, `sso`, or `social_google`.
    - `assurance` · string · required: The strength of that sign-in: `single_factor` or `strong`.
    - `authenticated_at` · date-time · required: When the sign-in happened.
    - `session_id` · uuid: The session that acted, when a person did.
    - `credential_id` · uuid: The credential that acted, when an agent did.
  - `idempotency_key` · string · required: The key the decision was recorded with.
  - `decided_at` · date-time · required
- `updated_at` · date-time · required: Moves on every change a reviewer should notice, escalation progress included, so a client refetches the review when it changes.
- `claim` · Claim: A reviewer's claim on a pending request: an advisory marker that they are looking at it. Absent once released, lapsed, or the request is no longer pending.
  - `id` · uuid · required
  - `membership_id` · uuid · required: The reviewer holding the claim.
  - `display_name` · string: The reviewer's name.
  - `claimed_at` · date-time · required
  - `expires_at` · date-time · required: When the claim lapses on its own.
- `presentation` · ResolvedToolPresentation: The presentation a request renders with and where it came from.
  - `source` · enum · required: `organization` for one of the organization's own entries, `built_in` for the shipped catalog. One of `built_in`, `organization`.
  - `id` · uuid: The organization entry, when the source is `organization`.
  - `title` · string
  - `fields` · array<ToolPresentationField> · required
    - `path` · string: A JSON pointer into the arguments. Required unless the field is a diff pair. Inside a list's `item` it is relative to the element (no leading slash; empty means the element itself).
    - `label` · string: Replaces the humanised key.
    - `kind` · enum · required: How one argument renders. `text` is a short value; `prose` long plain text; `markdown` and `html` are rendered (HTML in a sandbox that runs no script and loads nothing, with the source one click away); `code` is a mono block labelled with `language`; `shell` a command line; `diff` a unified diff at `path` or the pair at `before` and `after`; `path` a file path; `url` a link shown, never followed; `email` an address; `reference` an opaque identifier; `enum` a badge; `datetime` an instant in the reader's locale; `list` an array rendered per `item`; `json` pretty-printed structure. One of `text`, `prose`, `markdown`, `html`, `code`, `shell`, `diff`, `path`, `url`, `email`, `reference`, `enum`, `datetime`, `list`, `json`.
    - `language` · string: Labels a `code` field for the reader.
    - `before` · string: For `diff`: the pointer to the text before the change. Requires `after`; excludes `path`.
    - `after` · string: For `diff`: the pointer to the text after the change.
    - `item` · ToolPresentationField: One rendered argument. A field whose path is absent from a request's arguments is skipped; arguments no field names still show after the named ones, rendered by shape.
    - `role` · enum: `primary` fields make up the block a reviewer reads first; `detail` fields fold away underneath it. One of `primary`, `detail`.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/requests/{id}?wait=30s" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "agent_slug": "support-agent",
  "agent_instance_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "agent_name": "support-agent",
  "agent_instance_name": "ci-runner-07",
  "tool": "issue_refund",
  "server": "stripe",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "agent_reasoning": "Refunding the duplicate charge for jane@northwind.com.",
  "context": {
    "run_id": "4821",
    "framework": "claude-code"
  },
  "status": "approved",
  "deadline_at": "2026-09-09T12:00:00Z",
  "created_at": "2026-09-08T12:00:00Z",
  "decision": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "request_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "status": "approved",
    "note": "OK, but flag this account for review.",
    "channel": "web",
    "reviewer_display_name": "Chris",
    "reviewer_email": "reviewer@example.com",
    "authentication": {
      "method": "local_password",
      "assurance": "strong",
      "authenticated_at": "2026-09-08T12:02:11Z",
      "session_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "credential_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
    },
    "idempotency_key": "decision-7ab8c8ec",
    "decided_at": "2026-09-08T12:02:11Z"
  },
  "updated_at": "2026-09-08T12:05:00Z",
  "claim": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "display_name": "Chris",
    "claimed_at": "2026-09-08T12:01:30Z",
    "expires_at": "2026-09-08T12:16:30Z"
  },
  "presentation": {
    "source": "built_in",
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "title": "Edit {{/file_path}}",
    "fields": [
      {
        "path": "/body",
        "label": "Body",
        "kind": "text",
        "language": "python",
        "before": "/old_string",
        "after": "/new_string",
        "item": {},
        "role": "primary"
      }
    ]
  }
}
```

## Claim a request

`POST /api/v1/requests/{id}/claim`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `request.claim`

Marks a pending request as being looked at by you. Other reviewers see who has it, and its notifications pause. A claim is advisory: anyone allowed to decide the request can still decide it.

A claim lapses after the organization's claim timeout, 15 minutes by default. It also lapses at the request's deadline if that comes first. Claiming a request you already hold returns the same claim. You cannot claim a request another reviewer holds. Wait for their claim to lapse, or for them to release it.

Owners and reviewers can claim. If the request was routed to an escalation path, only the people on that path can claim it. Owners can always claim.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The request's id. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The claim you hold |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | You cannot claim this request. Your membership is inactive, you lack permission to decide, or the request was routed to other people |
| 404 | ErrorResponse | No such request |
| 409 | ErrorResponse | Another reviewer holds the claim, or the request is no longer pending |

Response body (200):

- `claim` · Claim · required: A reviewer's claim on a pending request: an advisory marker that they are looking at it. Absent once released, lapsed, or the request is no longer pending.
  - `id` · uuid · required
  - `membership_id` · uuid · required: The reviewer holding the claim.
  - `display_name` · string: The reviewer's name.
  - `claimed_at` · date-time · required
  - `expires_at` · date-time · required: When the claim lapses on its own.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/requests/{id}/claim" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "claim": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "display_name": "Chris",
    "claimed_at": "2026-09-08T12:01:30Z",
    "expires_at": "2026-09-08T12:16:30Z"
  }
}
```

## Release a claim

`DELETE /api/v1/requests/{id}/claim`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `request.claim`

Releases your claim on a request. Other reviewers then see it as unclaimed. Only the reviewer holding a claim can release it. Other reviewers' claims lapse on their own.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The request's id. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 204 |  | The claim is released |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Another reviewer holds the claim |
| 404 | ErrorResponse | No live claim on this request |

### Example

```bash
curl -X DELETE "$WITHHUMAN_URL/api/v1/requests/{id}/claim" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

## Approve or deny a request

`POST /api/v1/requests/{id}/decision`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `request.decide`

Records your decision on a pending request and returns once it is durably stored. The agent waiting on the request sees the decision on its next read.

Every call needs an `Idempotency-Key` header. Repeating a call with the same key returns the decision already recorded.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The request's id. |
| `Idempotency-Key` | header | string | yes | A key of your choosing that identifies this call, so a retry does not act twice. See Idempotency in the API overview. |

Request body:

- `status` · enum · required: The decision. One of `approved`, `denied`.
- `note` · string: A note for the agent. It is returned with the decision.
- `channel` · string: Where the decision was made, recorded on the decision, for example `web` or `slack`. Defaults to `web`.
- `outside_routing` · boolean: The reviewer's explicit acknowledgement that they are deciding outside the request's routing (break glass). Required, alongside request.decide.unrouted, when routing did not hand them the request; without it such a decision is refused with reason outside_routing_required. Part of the decision's intent under the idempotency key.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | DecisionOperation | The recorded decision |
| 400 | ErrorResponse | The `Idempotency-Key` header is missing, or `status` is not `approved` or `denied` |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | You cannot decide this request. Your membership is inactive, you lack permission to decide, the request was routed to other people, or your sign-in is too old or not strong enough. A routing refusal carries RoutingRejectionDetails in error.details, whose reason is not_targeted, break_glass_only, or outside_routing_required. |
| 404 | ErrorResponse | No such request |
| 409 | ErrorResponse | The request already has a decision or has expired, or the idempotency key was already used with different parameters |

Response body (200):

- `id` · uuid · required: The operation's id. The decision itself has its own.
- `organization_id` · uuid · required
- `request_id` · uuid · required: The request that was decided.
- `status` · enum · required: `applied` once the decision is stored. A `200` response always says `applied`: the call waits for the outcome, and a rejected decision comes back as an error. One of `pending`, `applied`, `rejected`.
- `error_code` · string: Why the decision was rejected. Set for `rejected` only.
- `error_message` · string: The rejection in plain text. Set for `rejected` only.
- `decision` · Decision: The outcome of an approval request and who produced it.
  - `id` · uuid · required
  - `request_id` · uuid · required
  - `organization_id` · uuid · required
  - `membership_id` · uuid: The member who decided. Absent when the pipeline decided, the request expired, or the agent cancelled it.
  - `status` · enum · required One of `approved`, `denied`, `expired`, `cancelled`.
  - `note` · string: A note the reviewer left for the agent, if any.
  - `channel` · string · required: Where the decision was made, as reported by the client, for example `web` or `slack`. `pipeline` when the pipeline decided, `system` when the request expired, `aap` when the agent cancelled it.
  - `reviewer_display_name` · string: The reviewer's name, when a person decided.
  - `reviewer_email` · email: The reviewer's email, when a person decided.
  - `authentication` · AuthContext · required: How a principal was signed in when it acted.
    - `method` · string · required: How the principal signed in, for example `local_password`, `sso`, or `social_google`.
    - `assurance` · string · required: The strength of that sign-in: `single_factor` or `strong`.
    - `authenticated_at` · date-time · required: When the sign-in happened.
    - `session_id` · uuid: The session that acted, when a person did.
    - `credential_id` · uuid: The credential that acted, when an agent did.
  - `idempotency_key` · string · required: The key the decision was recorded with.
  - `decided_at` · date-time · required
- `created_at` · date-time · required
- `completed_at` · date-time: When the decision was stored.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/requests/{id}/decision" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Idempotency-Key: 4f1c9a2e-7b3d-4e8f-a1c5-2d6b8e0f9a31" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "approved",
  "note": "OK, but flag this account for review.",
  "channel": "web",
  "outside_routing": true
}'
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "request_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "status": "applied",
  "error_code": "string",
  "error_message": "string",
  "decision": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "request_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "status": "approved",
    "note": "OK, but flag this account for review.",
    "channel": "web",
    "reviewer_display_name": "Chris",
    "reviewer_email": "reviewer@example.com",
    "authentication": {
      "method": "local_password",
      "assurance": "strong",
      "authenticated_at": "2026-09-08T12:02:11Z",
      "session_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "credential_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
    },
    "idempotency_key": "decision-7ab8c8ec",
    "decided_at": "2026-09-08T12:02:11Z"
  },
  "created_at": "2026-09-08T12:02:11Z",
  "completed_at": "2026-09-08T12:02:11Z"
}
```

## Retrieve a request's routing

`GET /api/v1/requests/{id}/routing`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `request.read`

Returns who was asked to review a request when it entered human review. If an escalation path took the request, the people on that path were notified, and only they can decide it. If no path applied, every reviewer who can decide was notified, and any of them can decide it. Routing is fixed when the request enters human review. Later changes to pipelines or paths do not move it.

Requests the pipeline decided automatically never entered human review and have no routing.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The request's id. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | ApprovalRouting | Where the request was routed |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | You cannot read requests |
| 404 | ErrorResponse | No such request, or it never reached human review |

Response body (200):

- `outcome` · enum · required: `escalated` when an escalation path took the request. `default_queue` when no path applied and every reviewer who can decide was asked. One of `escalated`, `default_queue`.
- `reason` · enum · required: How the outcome came about. `block_escalation`: the pipeline block that asked for a person named the path. `pipeline_default`: the pipeline's default path took it. `no_escalation`: the pipeline named no path. `path_inactive`: the named path had no active revision. `evaluation_error`: the path could not be resolved. The last three mean no path applied. One of `block_escalation`, `pipeline_default`, `no_escalation`, `path_inactive`, `evaluation_error`.
- `urgency` · enum · required: The urgency the request was routed with. Always `standard` today. One of `standard`, `interrupt`.
- `escalation_path_key` · string: The path that took the request. Present for `escalated` only.
- `escalation_path_name` · string: The name of the path revision that took the request. Present for `escalated` only.
- `escalation_path_revision_id` · uuid: The path revision that was active at the time. Present for `escalated` only.
- `pipeline_revision_id` · uuid: The pipeline revision that routed the request. Present for `escalated` only.
- `block_id` · uuid: The pipeline block that named the path. Present for `block_escalation` only.
- `targets` · array<ApprovalRoutingTarget> · required: The people and teams the pinned path reaches for this request, in path order: the levels whose request conditions hold for it, plus both branches of any working-hours or urgency test, at every level regardless of timing. Only these people, and `added_reviewers`, may claim or decide. Empty for `default_queue`. Empty for `escalated` means the path reaches nobody for this request and only a break-glass reviewer may decide it.
  - `type` · enum · required: `user` names a membership, `team` a team. One of `user`, `team`.
  - `id` · uuid · required
  - `name` · string · required: The person's display name or the team's name.
- `added_reviewers` · array<ApprovalReviewer> · required: People added to the request's routing after the snapshot was pinned, oldest first.
  - `membership_id` · uuid · required
  - `display_name` · string · required
  - `added_by_membership_id` · uuid · required
  - `added_by_display_name` · string · required
  - `created_at` · date-time · required
- `team_policies` · array<PinnedTeamPolicy> · required: The team escalation policy revisions pinned beside the path, one per team the path reaches for this request that had an active policy at routing time. A team absent here is notified all at once when a level reaches it. Empty for `default_queue`.
  - `team_id` · uuid · required
  - `team_name` · string · required
  - `policy_revision_id` · uuid · required
  - `revision` · int64 · required
- `escalation` · EscalationProgress | null · required: The escalation executor's current position. Null before its first tick and for `default_queue`.
- `created_at` · date-time · required: When the request entered human review.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/requests/{id}/routing" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "outcome": "escalated",
  "reason": "block_escalation",
  "urgency": "standard",
  "escalation_path_key": "oncall",
  "escalation_path_name": "On-call engineers",
  "escalation_path_revision_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "pipeline_revision_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "block_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "targets": [
    {
      "type": "user",
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "string"
    }
  ],
  "added_reviewers": [
    {
      "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "display_name": "string",
      "added_by_membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "added_by_display_name": "string",
      "created_at": "2026-09-08T12:02:11Z"
    }
  ],
  "team_policies": [
    {
      "team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "team_name": "string",
      "policy_revision_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "revision": 1
    }
  ],
  "escalation": {
    "sequence": 1,
    "phase": "level",
    "urgency": "standard",
    "current_node_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "level_ordinal": 1,
    "level_count": 1,
    "repeat": 1,
    "next_escalation_at": "2026-09-08T12:02:11Z",
    "updated_at": "2026-09-08T12:02:11Z",
    "teams": [
      {
        "team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "phase": "level",
        "current_node_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "level_ordinal": 1,
        "level_count": 1,
        "repeat": 1,
        "next_escalation_at": "2026-09-08T12:02:11Z"
      }
    ]
  },
  "created_at": "2026-09-08T12:00:00Z"
}
```

## List who a request could be handed to

`GET /api/v1/requests/{id}/reviewer-candidates`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `request.decide`

Active members who hold request.decide and are not yet reviewers of the request. Gated like deciding rather than by membership.read, so a targeted reviewer without the members page can still hand off.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The request's id. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | Candidates sorted by name |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `candidates` · array<object> · required
  - `membership_id` · uuid · required
  - `display_name` · string · required
  - `email` · string · required

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/requests/{id}/reviewer-candidates" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "candidates": [
    {
      "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "display_name": "string",
      "email": "string"
    }
  ]
}
```

## Add a reviewer to a pending request

`POST /api/v1/requests/{id}/reviewers`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `request.decide`

The handoff. Someone the request was routed to (or a holder of request.decide.unrouted) pulls one more active member who may decide into the request's routing. Widening never removes anyone and is audited as request.reviewer_added. Refused when the request is not pending or was never routed (409), when the actor is not targeted (403), or when the member is inactive or cannot decide (400).

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The request's id. |

Request body:

- `membership_id` · uuid · required

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | ApprovalReview | The request's review after the widening |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 409 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `routing` · ApprovalRouting: Who was asked to review a request when it entered human review. escalated pins an escalation path revision and the people it reaches for this request; default_queue means any authorized reviewer.
  - `outcome` · enum · required: `escalated` when an escalation path took the request. `default_queue` when no path applied and every reviewer who can decide was asked. One of `escalated`, `default_queue`.
  - `reason` · enum · required: How the outcome came about. `block_escalation`: the pipeline block that asked for a person named the path. `pipeline_default`: the pipeline's default path took it. `no_escalation`: the pipeline named no path. `path_inactive`: the named path had no active revision. `evaluation_error`: the path could not be resolved. The last three mean no path applied. One of `block_escalation`, `pipeline_default`, `no_escalation`, `path_inactive`, `evaluation_error`.
  - `urgency` · enum · required: The urgency the request was routed with. Always `standard` today. One of `standard`, `interrupt`.
  - `escalation_path_key` · string: The path that took the request. Present for `escalated` only.
  - `escalation_path_name` · string: The name of the path revision that took the request. Present for `escalated` only.
  - `escalation_path_revision_id` · uuid: The path revision that was active at the time. Present for `escalated` only.
  - `pipeline_revision_id` · uuid: The pipeline revision that routed the request. Present for `escalated` only.
  - `block_id` · uuid: The pipeline block that named the path. Present for `block_escalation` only.
  - `targets` · array<ApprovalRoutingTarget> · required: The people and teams the pinned path reaches for this request, in path order: the levels whose request conditions hold for it, plus both branches of any working-hours or urgency test, at every level regardless of timing. Only these people, and `added_reviewers`, may claim or decide. Empty for `default_queue`. Empty for `escalated` means the path reaches nobody for this request and only a break-glass reviewer may decide it.
    - `type` · enum · required: `user` names a membership, `team` a team. One of `user`, `team`.
    - `id` · uuid · required
    - `name` · string · required: The person's display name or the team's name.
  - `added_reviewers` · array<ApprovalReviewer> · required: People added to the request's routing after the snapshot was pinned, oldest first.
    - `membership_id` · uuid · required
    - `display_name` · string · required
    - `added_by_membership_id` · uuid · required
    - `added_by_display_name` · string · required
    - `created_at` · date-time · required
  - `team_policies` · array<PinnedTeamPolicy> · required: The team escalation policy revisions pinned beside the path, one per team the path reaches for this request that had an active policy at routing time. A team absent here is notified all at once when a level reaches it. Empty for `default_queue`.
    - `team_id` · uuid · required
    - `team_name` · string · required
    - `policy_revision_id` · uuid · required
    - `revision` · int64 · required
  - `escalation` · EscalationProgress | null · required: The escalation executor's current position. Null before its first tick and for `default_queue`.
  - `created_at` · date-time · required: When the request entered human review.
- `added_reviewers` · array<ApprovalReviewer> · required: People added to the request's routing after it was pinned, oldest first. The same list the routing carries.
  - `membership_id` · uuid · required
  - `display_name` · string · required
  - `added_by_membership_id` · uuid · required
  - `added_by_display_name` · string · required
  - `created_at` · date-time · required
- `viewer` · ReviewViewer · required: The reading person's own standing on the request. targeted says whether the pinned path reaches them for this request (directly, through a team, or by a later widening). decidable says whether a decision from them would pass the routing rule outright (yes), only with the outside_routing acknowledgement and request.decide.unrouted (break_glass), or not at all (no). reason names the rule in the way; it is absent when decidable is yes or the request is no longer pending. The decision itself still checks membership status, self-approval, and authentication freshness.
  - `targeted` · boolean · required
  - `decidable` · enum · required One of `yes`, `break_glass`, `no`.
  - `reason` · enum One of `not_targeted`, `break_glass_only`, `no_permission`.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/requests/{id}/reviewers" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
}'
```

200 response

```json
{
  "routing": {
    "outcome": "escalated",
    "reason": "block_escalation",
    "urgency": "standard",
    "escalation_path_key": "oncall",
    "escalation_path_name": "On-call engineers",
    "escalation_path_revision_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "pipeline_revision_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "block_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "targets": [
      {
        "type": "user",
        "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "name": "string"
      }
    ],
    "added_reviewers": [
      {
        "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "display_name": "string",
        "added_by_membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "added_by_display_name": "string",
        "created_at": "2026-09-08T12:02:11Z"
      }
    ],
    "team_policies": [
      {
        "team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "team_name": "string",
        "policy_revision_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "revision": 1
      }
    ],
    "escalation": {
      "sequence": 1,
      "phase": "level",
      "urgency": "standard",
      "current_node_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "level_ordinal": 1,
      "level_count": 1,
      "repeat": 1,
      "next_escalation_at": "2026-09-08T12:02:11Z",
      "updated_at": "2026-09-08T12:02:11Z",
      "teams": [
        {
          "team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "phase": "level",
          "current_node_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "level_ordinal": 1,
          "level_count": 1,
          "repeat": 1,
          "next_escalation_at": "2026-09-08T12:02:11Z"
        }
      ]
    },
    "created_at": "2026-09-08T12:00:00Z"
  },
  "added_reviewers": [
    {
      "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "display_name": "string",
      "added_by_membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "added_by_display_name": "string",
      "created_at": "2026-09-08T12:02:11Z"
    }
  ],
  "viewer": {
    "targeted": true,
    "decidable": "yes",
    "reason": "not_targeted"
  }
}
```

## Read why a request needs a human and what has happened to it since

`GET /api/v1/requests/{id}/timeline`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `request.read`

Hosted edition only. The open edition answers 404.

Product read only, gated by request.read alone. The pipeline's conclusion and the block that asked for a human, every judge assessment, the escalation steps, and the claim history, told from the request's own audit events, so a reviewer does not need audit.read to understand why they are being asked. Raw evaluation traces stay in the audit log. Routing, added reviewers, and the reader's standing are the review (`GET /api/v1/requests/{id}/review`), which every edition serves.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The request's id. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | RequestTimeline | The request's timeline |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `pipeline` · PipelineReview: The completed evaluation as a reviewer reads it. The terminal block is the one whose outcome ended the pipeline; its fields are absent when the pipeline fell through its end or never ran.
  - `outcome` · enum · required One of `approve`, `deny`, `human`.
  - `reason_code` · string · required: Why the pipeline stopped, for example block_outcome, block_timeout, retries_exhausted, end_of_branch (the request entered a branch and none of its blocks decided), no_active_revision.
  - `blocks_evaluated` · integer · required
  - `terminal_block_key` · string
  - `terminal_block_name` · string
  - `terminal_block_type` · enum One of `always`, `cel`, `llm_judge`, `webhook`, `branch`.
  - `terminal_block_reason` · string: The sentence the pipeline author configured on the terminal block, written for the reviewer.
  - `endpoint_reason` · string: A webhook endpoint's own explanation of its answer, when the terminal block was a webhook that gave one.
  - `terminal_block_error_code` · string: Present when the terminal block failed instead of answering, for example `evaluation_error` or `timeout`. A failed block always sends the request to a human, so a reviewer should not read it as a match.
  - `terminal_pipeline` · object: The section of the effective pipeline that held the terminal block, at the revision that ran. Enough to read that revision and find the block in it.
    - `scope` · enum · required One of `organization`, `agent`.
    - `agent_slug` · string: Present when the scope is `agent`.
    - `revision` · int64 · required
  - `completed_at` · date-time · required
- `assessments` · array<JudgeAssessment> · required
  - `block_key` · string · required
  - `block_name` · string
  - `verdict` · enum · required One of `approve`, `deny`, `escalate`.
  - `confidence` · enum · required One of `low`, `medium`, `high`.
  - `rationale` · string · required
  - `model_label` · string: The judge model's name when it judged the request.
  - `provider` · string
  - `model` · string
  - `downgraded_for_confidence` · boolean · required: True when the confidence fell below the block's threshold and the verdict was treated as escalate.
  - `evaluated_at` · date-time · required
- `claims` · array<ClaimEvent> · required
  - `kind` · enum · required One of `claimed`, `released`, `lapsed`.
  - `actor_display_name` · string
  - `occurred_at` · date-time · required
- `escalations` · array<EscalationEvent> · required: What the escalation executor did for the request, oldest first. Empty for a default queue request.
  - `kind` · enum · required One of `level_entered`, `urgency_raised`, `deferred`, `repeated`, `exhausted`.
  - `team_id` · string
  - `node_id` · string
  - `level_ordinal` · integer: The level's position within its own document, one-based (level_entered only).
  - `level_count` · integer: How many levels the document has (level_entered only).
  - `urgency` · enum: The urgency the level was entered at, or the urgency raised to. One of `standard`, `interrupt`.
  - `until` · date-time: When a deferred walker resumes (deferred only).
  - `iteration` · integer: Which repeat of the document is starting (repeated only).
  - `occurred_at` · date-time · required
- `audit_event_count` · integer · required: How many audit events the request has in total, for the link into the audit log.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/requests/{id}/timeline" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "pipeline": {
    "outcome": "approve",
    "reason_code": "string",
    "blocks_evaluated": 1,
    "terminal_block_key": "string",
    "terminal_block_name": "string",
    "terminal_block_type": "always",
    "terminal_block_reason": "string",
    "endpoint_reason": "string",
    "terminal_block_error_code": "string",
    "terminal_pipeline": {
      "scope": "organization",
      "agent_slug": "string",
      "revision": 1
    },
    "completed_at": "2026-09-08T12:02:11Z"
  },
  "assessments": [
    {
      "block_key": "string",
      "block_name": "string",
      "verdict": "approve",
      "confidence": "low",
      "rationale": "string",
      "model_label": "string",
      "provider": "string",
      "model": "string",
      "downgraded_for_confidence": true,
      "evaluated_at": "2026-09-08T12:02:11Z"
    }
  ],
  "claims": [
    {
      "kind": "claimed",
      "actor_display_name": "string",
      "occurred_at": "2026-09-08T12:02:11Z"
    }
  ],
  "escalations": [
    {
      "kind": "level_entered",
      "team_id": "string",
      "node_id": "string",
      "level_ordinal": 1,
      "level_count": 1,
      "urgency": "standard",
      "until": "2026-09-08T12:02:11Z",
      "iteration": 1,
      "occurred_at": "2026-09-08T12:02:11Z"
    }
  ],
  "audit_event_count": 1
}
```
