# Agent Approval Protocol

What an adapter calls on behalf of a running agent. Create an instance and get its credential, ask for approval of a tool call, and wait for the answer.

The Agent Approval Protocol (AAP) is the open protocol between an agent
runtime and an approval provider. An adapter sits between the agent and its
tools. When the agent calls a tool that needs approval, the
adapter holds the call, describes it to the provider, and waits. The provider
collects a decision from a person, or from the organization's approval
pipeline, and hands it back. The adapter then runs the call or fails it.

The protocol carries decisions only. It never executes a tool or forwards its
result. Approving or denying is the whole job, which keeps the surface to a
handful of calls that any runtime can implement and any provider can serve.
withHuman is one provider; an adapter written against this page works with
any other.

A typical exchange:

1. The adapter obtains a credential, either from `withhuman agent install <runtime>` on a
   developer machine or by [creating an instance](#mintAgentInstance) with
   a provisioner token.
2. For a tool call, the adapter [creates an approval request](#createApprovalRequest)
   describing the tool and its arguments. The pipeline may decide on the
   spot; otherwise the request is pending.
3. It [retrieves the request](#getApprovalDecision) with a `wait`, repeating
   until the status is `approved`, `denied`, `expired`, or `cancelled`, and
   acts on it.
4. If the adapter stops waiting first (its runtime was interrupted or timed
   out), it [cancels the request](#cancelApprovalRequest) so reviewers stop
   seeing it.

## Create an agent instance

`POST /api/aap/v1/instances`

Auth: Bearer agent credential (`Authorization: Bearer $WITHHUMAN_TOKEN`)

Creates a new instance of the agent the provisioner token belongs to and returns it together with its agent credential. Use this from a fleet or platform that starts agent replicas itself, so each replica gets a credential of its own. The bearer token must be a provisioner token (`whp_`); an agent credential is rejected.

### Request

Request body (optional):

- `instance_name` · string: A name for the instance, unique within the agent. Reviewers see it next to the agent's name.
- `instance_metadata` · object: Any JSON object to store with the instance, such as a region or pod name.
- `ttl_seconds` · int64: How long the credential stays valid, in seconds. A value above the provider's default is reduced to the default.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | AgentInstanceCredential | The new instance and its credential. The credential's token appears only in this response. |
| 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. |
| 409 |  | An instance with this name already exists for the agent |

Response body (201):

- `agent_instance` · AgentInstance · required: One running copy of an agent.
  - `id` · uuid · required
  - `organization_id` · uuid · required
  - `agent_slug` · string · required: The slug of the agent this is an instance of.
  - `name` · string · required: Unique within the agent.
  - `status` · enum · required: A disabled instance cannot create requests. One of `active`, `disabled`.
  - `metadata` · object · required: The JSON stored when the instance was created.
  - `created_at` · date-time · required
  - `last_seen_at` · date-time · required: The last time this instance called the API.
- `credential` · IssuedCredential · required
  - `id` · uuid · required: Identifies the credential, for example when revoking it.
  - `token` · string · required: The secret. It appears only in this response and cannot be retrieved again.
  - `expires_at` · date-time · required: When the credential stops working.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/aap/v1/instances" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "instance_name": "ci-runner-07",
  "instance_metadata": {
    "region": "eu-west"
  },
  "ttl_seconds": 86400
}'
```

201 response

```json
{
  "agent_instance": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "agent_slug": "string",
    "name": "ci-runner-07",
    "status": "active",
    "metadata": {
      "region": "eu-west"
    },
    "created_at": "2026-09-08T12:02:11Z",
    "last_seen_at": "2026-09-08T12:02:11Z"
  },
  "credential": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "token": "whc_live_3f6b9c1d0e7a4b2c",
    "expires_at": "2026-09-08T12:02:11Z"
  }
}
```

## Create an approval request

`POST /api/aap/v1/requests`

Auth: Bearer agent credential (`Authorization: Bearer $WITHHUMAN_TOKEN`)

Requires: `request.create`

Asks for approval to run a tool call. The organization's approval pipeline evaluates the request first and may approve or deny it on the spot, in which case the response already carries the decision. Otherwise the request goes to reviewers and comes back as `pending`; retrieve it with a `wait` to learn the outcome. A request nobody decides before its timeout becomes `expired`: the adapter does not run the call and tells the agent the approval timed out. An adapter that stops waiting should cancel the request so reviewers stop seeing it. Requires an `Idempotency-Key` header scoped to this operation and the authenticated instance. Reusing it with changed input returns 409.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `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:

- `tool` · string · required: The name under which the tool is defined: the name an MCP server advertises in `tools/list`, or the runtime's own name for a built-in tool such as `Bash`. Never the runtime's joined spelling such as `mcp__stripe__issue_refund`.
- `server` · string: The alias of the MCP server that defines the tool, as the runtime configured it. Present only for tools served over MCP. A label the adapter observed, not a verified identity.
- `arguments` · object · required: The exact arguments the tool will run with if approved. Reviewers see this as the description of the action, so it must be complete.
- `agent_reasoning` · string: The agent's own explanation of why it wants to do this. Reviewers see it as a claim from the agent, separate from the arguments.
- `context` · object: Where the call comes from, as observed by the adapter rather than stated by the agent: for example the runtime, session id, or working directory.
- `timeout` · string · required: How long the request may wait for a decision, as a duration such as `30m` or `24h`. Between one second and seven days. Once it passes, the request expires.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | AAPApprovalRequest | The request, either already decided by the pipeline or pending review |
| 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. |
| 409 |  | The idempotency key was already used for a different request |
| 429 |  | The organization has reached its limit of pending requests |

Response body (201):

- `id` · uuid · required
- `tool` · string · required
- `server` · string
- `arguments` · object · required
- `timeout` · string · required
- `agent_reasoning` · string
- `context` · object
- `status` · enum · required One of `pending`, `approved`, `denied`, `expired`, `cancelled`.
- `created_at` · date-time · required
- `deadline_at` · date-time · required
- `decision` · AAPDecision: Immutable decision. Approved calls must start within five minutes of decided_at. Reads and retries never extend expires_at.
  - `status` · enum · required One of `approved`, `denied`, `expired`, `cancelled`.
  - `note` · string
  - `decided_at` · date-time · required
  - `expires_at` · date-time

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/aap/v1/requests" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN" \
  -H "Idempotency-Key: 4f1c9a2e-7b3d-4e8f-a1c5-2d6b8e0f9a31" \
  -H "Content-Type: application/json" \
  -d '{
  "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"
  },
  "timeout": "30m"
}'
```

201 response: Pending review

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "pending",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z"
}
```

201 response: Approved with a five-minute execution window

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "approved",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z",
  "decision": {
    "status": "approved",
    "note": "Refund the duplicate charge.",
    "decided_at": "2026-09-17T12:02:00Z",
    "expires_at": "2026-09-17T12:07:00Z"
  }
}
```

## Retrieve an approval request

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

Auth: Bearer agent credential (`Authorization: Bearer $WITHHUMAN_TOKEN`)

Requires: `request.read`

Returns the request and, once it has been decided, its decision. Pass `wait` to hold the response until the request is decided or the wait runs out, whichever comes first. Only the instance that created the request may retrieve it. A request stops changing once it is `approved`, `denied`, `expired`, or `cancelled`.

### 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 | AAPApprovalRequest | The request in its current state |
| 401 | 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):

- `id` · uuid · required
- `tool` · string · required
- `server` · string
- `arguments` · object · required
- `timeout` · string · required
- `agent_reasoning` · string
- `context` · object
- `status` · enum · required One of `pending`, `approved`, `denied`, `expired`, `cancelled`.
- `created_at` · date-time · required
- `deadline_at` · date-time · required
- `decision` · AAPDecision: Immutable decision. Approved calls must start within five minutes of decided_at. Reads and retries never extend expires_at.
  - `status` · enum · required One of `approved`, `denied`, `expired`, `cancelled`.
  - `note` · string
  - `decided_at` · date-time · required
  - `expires_at` · date-time

### Example

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

200 response: Pending review

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "pending",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z"
}
```

200 response: Approved with a five-minute execution window

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "approved",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z",
  "decision": {
    "status": "approved",
    "note": "Refund the duplicate charge.",
    "decided_at": "2026-09-17T12:02:00Z",
    "expires_at": "2026-09-17T12:07:00Z"
  }
}
```

## Cancel an approval request

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

Auth: Bearer agent credential (`Authorization: Bearer $WITHHUMAN_TOKEN`)

Requires: `request.create`

Withdraws a pending request. Use it when the runtime that asked stops waiting for the answer: it was interrupted, timed out locally, or is shutting down. Reviewers stop seeing the request, notifications stop, and anyone holding it is released. Only the instance that created the request may cancel it; another instance of the same agent gets a `403`. A cancelled request is final and answers `cancelled` with a protocol decision, so a long poll in flight returns at once. Cancelling again returns the same cancelled request. A request that was already `approved`, `denied`, or `expired` is unchanged and answers `409` with code `already_terminal`. This is an optional capability of the Agent Approval Protocol: adapters must treat a `404` or `405` from a provider that does not offer it as nothing to do.

### Request

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

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | AAPApprovalRequest | The request, now cancelled |
| 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 |  | The request was already decided or expired (code `already_terminal`) |

Response body (200):

- `id` · uuid · required
- `tool` · string · required
- `server` · string
- `arguments` · object · required
- `timeout` · string · required
- `agent_reasoning` · string
- `context` · object
- `status` · enum · required One of `pending`, `approved`, `denied`, `expired`, `cancelled`.
- `created_at` · date-time · required
- `deadline_at` · date-time · required
- `decision` · AAPDecision: Immutable decision. Approved calls must start within five minutes of decided_at. Reads and retries never extend expires_at.
  - `status` · enum · required One of `approved`, `denied`, `expired`, `cancelled`.
  - `note` · string
  - `decided_at` · date-time · required
  - `expires_at` · date-time

### Example

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

200 response: Cancelled by the adapter

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "cancelled",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z",
  "decision": {
    "status": "cancelled",
    "note": "The agent stopped waiting.",
    "decided_at": "2026-09-17T12:02:00Z"
  }
}
```
