# Agents

Agents are the AI systems that ask for approval. Each agent has instances, and each instance holds a credential it uses to call the API.

Tokens are returned once, when issued, and are never shown again.

[Register an instance](#createAgentInstance) to get a credential directly, or
[create a provisioner token](#createAgentProvisioner) so each replica can
create its own instance through the
[Agent Approval Protocol (AAP)](/docs/reference/agent-approval-protocol).

## List agents

`GET /api/v1/agents`

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

Requires: `agent.read`

Returns the organization's agents. Each entry carries the agent's instance counts, when it was last seen, and how many requests it has made. Active agents come first, then disabled ones, then archived ones, each group sorted by name. Archived agents are left out unless `status` says otherwise.

An agent is live when one of its instances holds a credential that is neither expired nor revoked. `last_seen_at` is the newest authenticated call from any of its instances. Agents only call in when they have something to approve, so a quiet agent is not a broken one.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string | no | Which agents to return. `live` (the default) returns active and disabled agents, `archived` returns only archived ones, and `all` returns every agent. |
| `limit` | query | integer | no | How many agents to return. Defaults to 50. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The agents |
| 400 | ErrorResponse | `status` is not live, archived, or all |
| 403 | ErrorResponse | You cannot read agents |

Response body (200):

- `agents` · array<Agent> · required

### Example

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

200 response

```json
{
  "agents": [
    {
      "slug": "support-bot",
      "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Support bot",
      "status": "active",
      "created_at": "2026-09-08T12:02:11Z",
      "updated_at": "2026-09-08T12:02:11Z",
      "archived_at": "2026-09-08T12:02:11Z",
      "instance_count": 0,
      "live_instance_count": 0,
      "last_seen_at": "2026-09-08T12:02:11Z",
      "recent_request_count": 0,
      "pending_request_count": 0,
      "request_trend": [
        0
      ]
    }
  ]
}
```

## Create an agent

`POST /api/v1/agents`

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

Requires: `agent.write`

Creates an agent. An agent is a stable identity for one AI system, such as a support bot or a deploy assistant. Its slug is the identity: chosen here, never changed, never reused. Every agent owns one approval pipeline, created empty with the agent.

Creating an agent does not issue a credential. Register an instance, or create a provisioner token, to let it call the API.

### Request

Request body:

- `slug` · string · required: The agent's identity. Lowercase letters, digits, dots, underscores and hyphens, up to 63 characters. It cannot change and is never reused, so a slug an archived agent holds is refused.
- `name` · string · required: A display name. Reviewers see it on every request the agent makes. It can be changed later and must be unique among the organization's live agents.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | Agent | The new agent |
| 403 | ErrorResponse | You cannot create agents |
| 409 | ErrorResponse | The slug is taken by a live or archived agent (`agent_slug_taken`), or a live agent already has the name (`agent_name_taken`) |
| 429 |  | The organization is at its agent limit |

Response body (201):

- `slug` · string · required: The agent's identity, chosen once at creation. It never changes and is never reused, not even after the agent is archived.
- `organization_id` · uuid · required
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the agent.
- `name` · string · required: The display name. Reviewers see it on every request. Editable, and unique among the organization's live agents.
- `status` · enum · required: A `disabled` agent's instances cannot authenticate until it is enabled again. An `archived` agent is retired: nothing of it authenticates, nothing can be enrolled under it, and its status cannot change until it is restored. One of `active`, `disabled`, `archived`.
- `created_at` · date-time · required
- `updated_at` · date-time · required
- `archived_at` · date-time: When the agent was archived. Present exactly when `status` is `archived`.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/agents" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "slug": "support-bot",
  "name": "Support bot"
}'
```

201 response

```json
{
  "slug": "support-bot",
  "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "name": "Support bot",
  "status": "active",
  "created_at": "2026-09-08T12:02:11Z",
  "updated_at": "2026-09-08T12:02:11Z",
  "archived_at": "2026-09-08T12:02:11Z"
}
```

## Retrieve an agent

`GET /api/v1/agents/{slug}`

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

Requires: `agent.read`

Returns one agent with a page of instances, each instance's credentials, and its provisioner tokens. Secrets are never included. Instances are sorted by last seen, then creation time and id, newest first. Counts and connection state cover all instances, independent of the page and filters.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `slug` | path | string | yes | The agent's slug: the identity chosen when it was created, never changed and never reused. |
| `instance_limit` | query | integer | no | Number of instances in this page. |
| `instance_offset` | query | integer | no | Number of matching instances to skip. |
| `instance_q` | query | string | no | Case-insensitive literal substring of the instance name. |
| `instance_status` | query | string | no | Filter by whether an instance is enabled or disabled, independently of its credentials. |
| `provisioner_limit` | query | integer | no | Number of provisioner tokens in this page, sorted by issued time and id, newest first. |
| `provisioner_offset` | query | integer | no | Number of matching provisioner tokens to skip. |
| `provisioner_q` | query | string | no | Case-insensitive literal substring of the token identifier or displayed identifier suffix. Searches identifiers, never secrets. |
| `provisioner_status` | query | string | no | Token state, with revocation taking precedence over expiry. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | AgentDetail | The agent |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | You cannot read agents |
| 404 | ErrorResponse | No such agent |

Response body (200):

- `slug` · string · required: The agent's identity, chosen once at creation. It never changes and is never reused, not even after the agent is archived.
- `organization_id` · uuid · required
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the agent.
- `name` · string · required: The display name. Reviewers see it on every request. Editable, and unique among the organization's live agents.
- `status` · enum · required: A `disabled` agent's instances cannot authenticate until it is enabled again. An `archived` agent is retired: nothing of it authenticates, nothing can be enrolled under it, and its status cannot change until it is restored. One of `active`, `disabled`, `archived`.
- `created_at` · date-time · required
- `updated_at` · date-time · required
- `archived_at` · date-time: When the agent was archived. Present exactly when `status` is `archived`.
- `provisioner_count` · integer · required: All provisioner tokens owned by this agent.
- `matching_provisioner_count` · integer · required: Tokens matching the current identifier and status filters before pagination.
- `instance_count` · integer · required: All instances owned by this agent.
- `live_instance_count` · integer · required: Active instances with at least one live credential across all pages.
- `matching_instance_count` · integer · required: Instances matching the current search and status filters before pagination.
- `request_count` · integer · required: All requests made by this agent.
- `created_by_actor_type` · enum One of `human`, `api_key`.
- `created_by_display_name` · string · required: The name of the human or organization key that created the agent.
- `instances` · array<AgentInstance> · required: The requested page of instances, sorted by last seen, creation time, and id, newest first.
- `provisioners` · array<AgentProvisioner> · required: The requested page of provisioner tokens, sorted by issued time and id, newest first. Secrets are never included.
  - `id` · uuid · required
  - `organization_id` · uuid · required
  - `agent_slug` · string · required: The slug of the agent the token creates instances of.
  - `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the token.
  - `issued_at` · date-time · required
  - `expires_at` · date-time · required: When the token stops working.
  - `last_used_at` · date-time: The last time the token created an instance.
  - `revoked_at` · date-time: When it was revoked. Absent while it is not.
- `recent_request_count` · integer · required: Requests made in the last seven days.
- `pending_request_count` · integer · required: Requests still waiting for a decision.

### Example

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

200 response

```json
{
  "slug": "support-bot",
  "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "name": "Support bot",
  "status": "active",
  "created_at": "2026-09-08T12:02:11Z",
  "updated_at": "2026-09-08T12:02:11Z",
  "archived_at": "2026-09-08T12:02:11Z",
  "provisioner_count": 0,
  "matching_provisioner_count": 0,
  "instance_count": 0,
  "live_instance_count": 0,
  "matching_instance_count": 0,
  "request_count": 0,
  "created_by_actor_type": "human",
  "created_by_display_name": "string",
  "instances": [
    {
      "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",
      "credentials": [
        {
          "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "issued_at": "2026-09-08T12:02:11Z",
          "expires_at": "2026-09-08T12:02:11Z",
          "last_used_at": "2026-09-08T12:02:11Z",
          "revoked_at": "2026-09-08T12:02:11Z"
        }
      ],
      "recent_request_count": 0,
      "request_trend": [
        0
      ]
    }
  ],
  "provisioners": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "agent_slug": "string",
      "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "issued_at": "2026-09-08T12:02:11Z",
      "expires_at": "2026-09-08T12:02:11Z",
      "last_used_at": "2026-09-08T12:02:11Z",
      "revoked_at": "2026-09-08T12:02:11Z"
    }
  ],
  "recent_request_count": 0,
  "pending_request_count": 0
}
```

## Update an agent

`PATCH /api/v1/agents/{slug}`

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

Requires: `agent.write`

Changes an agent's status or its display name, the two fields that can change after creation. The slug never changes. Who manages an agent is decided by the role assignments at the agent, not by a property of it.

Disabling an agent keeps its instances and credentials in place, but none of them can authenticate until the agent is enabled again. An archived agent is past both switches; restore it first.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `slug` | path | string | yes | The agent's slug: the identity chosen when it was created, never changed and never reused. |

Request body:

- `status` · enum: `active` or `disabled`. One of `active`, `disabled`.
- `name` · string: A new display name, unique among the organization's live agents. Audited as `agent.renamed`.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | Agent | The updated agent |
| 400 | ErrorResponse | Neither status nor name was given, or the status is invalid |
| 403 | ErrorResponse | You cannot change agents |
| 404 | ErrorResponse | No such agent |
| 409 | ErrorResponse | The agent is archived (`agent_archived`), or a live agent already has the name (`agent_name_taken`) |

Response body (200):

- `slug` · string · required: The agent's identity, chosen once at creation. It never changes and is never reused, not even after the agent is archived.
- `organization_id` · uuid · required
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the agent.
- `name` · string · required: The display name. Reviewers see it on every request. Editable, and unique among the organization's live agents.
- `status` · enum · required: A `disabled` agent's instances cannot authenticate until it is enabled again. An `archived` agent is retired: nothing of it authenticates, nothing can be enrolled under it, and its status cannot change until it is restored. One of `active`, `disabled`, `archived`.
- `created_at` · date-time · required
- `updated_at` · date-time · required
- `archived_at` · date-time: When the agent was archived. Present exactly when `status` is `archived`.

### Example

```bash
curl -X PATCH "$WITHHUMAN_URL/api/v1/agents/{slug}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "active",
  "name": "Support bot"
}'
```

200 response

```json
{
  "slug": "support-bot",
  "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "name": "Support bot",
  "status": "active",
  "created_at": "2026-09-08T12:02:11Z",
  "updated_at": "2026-09-08T12:02:11Z",
  "archived_at": "2026-09-08T12:02:11Z"
}
```

## Archive an agent

`DELETE /api/v1/agents/{slug}`

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

Requires: `agent.write`

Archives the agent. Every live credential and provisioner token is revoked. Nothing is deleted: the agent is still listed with `status=archived` or `all`, it can still be retrieved with its instances and revoked credentials, and its requests and audit trail stay readable. But no instance or provisioner token can be created for it and its status cannot change. Its name is freed for a new agent; its slug is not, and never will be.

The agent's approval pipeline is archived with it: every revision is stamped and the active one deactivated. The archived revisions stay readable but cannot be activated. Restore the agent to bring the pipeline back.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `slug` | path | string | yes | The agent's slug: the identity chosen when it was created, never changed and never reused. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 204 |  | The agent is archived |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot change agents |
| 404 | ErrorResponse | No such agent |
| 409 | ErrorResponse | The agent is already archived (`agent_archived`) |

### Example

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

## Restore an archived agent

`POST /api/v1/agents/{slug}/restore`

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

Requires: `agent.write`

Brings an archived agent back to `active`, together with its approval pipeline: every archived revision is unfrozen and the latest one becomes active again. Credentials and provisioner tokens revoked when the agent was archived stay revoked; register instances or connect them again to let it call the API. The name must still be free among the organization's live agents.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `slug` | path | string | yes | The agent's slug: the identity chosen when it was created, never changed and never reused. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | Agent | The restored agent |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot change agents |
| 404 | ErrorResponse | No such agent |
| 409 | ErrorResponse | The agent is not archived (`agent_not_archived`), or a live agent took its name meanwhile (`agent_name_taken`) |

Response body (200):

- `slug` · string · required: The agent's identity, chosen once at creation. It never changes and is never reused, not even after the agent is archived.
- `organization_id` · uuid · required
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the agent.
- `name` · string · required: The display name. Reviewers see it on every request. Editable, and unique among the organization's live agents.
- `status` · enum · required: A `disabled` agent's instances cannot authenticate until it is enabled again. An `archived` agent is retired: nothing of it authenticates, nothing can be enrolled under it, and its status cannot change until it is restored. One of `active`, `disabled`, `archived`.
- `created_at` · date-time · required
- `updated_at` · date-time · required
- `archived_at` · date-time: When the agent was archived. Present exactly when `status` is `archived`.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/agents/{slug}/restore" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "slug": "support-bot",
  "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "name": "Support bot",
  "status": "active",
  "created_at": "2026-09-08T12:02:11Z",
  "updated_at": "2026-09-08T12:02:11Z",
  "archived_at": "2026-09-08T12:02:11Z"
}
```

## Update an instance

`PATCH /api/v1/agent_instances/{id}`

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

Requires: `agent.write`

Changes an instance's status or its display name, the two fields that can change after enrollment. The agent and its other instances are not affected.

Disabling an instance keeps its credentials, but none of them can authenticate until it is enabled again. Renaming changes what the instance is called from now on: nothing addresses an instance by name after enrollment, so the running agent needs no change, and requests already made keep the name the instance had at the time.

### Request

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

Request body:

- `status` · enum: `active` or `disabled`. One of `active`, `disabled`.
- `name` · string: A new display name, unique within the agent. Audited as `agent_instance.renamed`.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | AgentInstance | The updated instance |
| 400 | ErrorResponse | Neither status nor name was given, or the status is invalid |
| 403 | ErrorResponse | You cannot change agents |
| 404 | ErrorResponse | No such instance |
| 409 | ErrorResponse | The instance's agent is archived (`agent_archived`), or another instance of the agent already has the name (`agent_instance_name_taken`) |

Response body (200):

- `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.

### Example

```bash
curl -X PATCH "$WITHHUMAN_URL/api/v1/agent_instances/{id}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "active",
  "name": "support-server-01"
}'
```

200 response

```json
{
  "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"
}
```

## Register an instance

`POST /api/v1/agents/{slug}/instances`

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

Requires: `agent.credential.issue`

Registers an instance of an agent and issues its credential. An instance is one running copy of the agent, such as a CI runner or a pod. The credential's token is returned once, in this response. Store it where the instance can read it. It cannot be retrieved again.

Use this when you set up an instance by hand. For a fleet that starts replicas itself, create a provisioner token instead and let each replica create its own instance through the Agent Approval Protocol (AAP).

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `slug` | path | string | yes | The agent's slug: the identity chosen when it was created, never changed and never reused. |

Request body:

- `name` · string · required: A name for the instance, unique within the agent. Reviewers see it next to the agent's name.
- `metadata` · object: Any JSON object to store with the instance, such as a region or pod name.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | AgentInstanceCredential | The new instance and its credential. The token appears only in this response. |
| 403 | ErrorResponse | You cannot issue credentials |
| 404 | ErrorResponse | No such agent |
| 409 | ErrorResponse | The agent is archived (`agent_archived`) |

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/v1/agents/{slug}/instances" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "ci-runner-07",
  "metadata": {
    "region": "eu-west"
  }
}'
```

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 a provisioner token

`POST /api/v1/agents/{slug}/provisioners`

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

Requires: `agent.credential.issue`

Creates a provisioner token for an agent. A provisioner token lets a fleet or platform create instances of this agent on its own, each with a credential of its own, through the Agent Approval Protocol (AAP). It cannot create approval requests itself.

The token is returned once, in this response. It cannot be retrieved again.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `slug` | path | string | yes | The agent's slug: the identity chosen when it was created, never changed and never reused. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | AgentProvisionerCredential | The provisioner and its token. The token appears only in this response. |
| 403 | ErrorResponse | You cannot issue credentials |
| 404 | ErrorResponse | No such agent |
| 409 | ErrorResponse | The agent is archived (`agent_archived`) |

Response body (201):

- `provisioner` · AgentProvisioner · required: A provisioner token as the agent page shows it. The token is never included.
  - `id` · uuid · required
  - `organization_id` · uuid · required
  - `agent_slug` · string · required: The slug of the agent the token creates instances of.
  - `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the token.
  - `issued_at` · date-time · required
  - `expires_at` · date-time · required: When the token stops working.
  - `last_used_at` · date-time: The last time the token created an instance.
  - `revoked_at` · date-time: When it was revoked. Absent while it is not.
- `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/v1/agents/{slug}/provisioners" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

201 response

```json
{
  "provisioner": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "agent_slug": "string",
    "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "issued_at": "2026-09-08T12:02:11Z",
    "expires_at": "2026-09-08T12:02:11Z",
    "last_used_at": "2026-09-08T12:02:11Z",
    "revoked_at": "2026-09-08T12:02:11Z"
  },
  "credential": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "token": "whc_live_3f6b9c1d0e7a4b2c",
    "expires_at": "2026-09-08T12:02:11Z"
  }
}
```

## Revoke a provisioner token

`DELETE /api/v1/agent-provisioners/{id}`

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

Requires: `agent.credential.revoke`

Revokes a provisioner token. It stops working immediately. The instances it already created, and their credentials, are not affected.

### Request

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

### Response

| Status | Body | Description |
| --- | --- | --- |
| 204 |  | The token is revoked |
| 403 | ErrorResponse | You cannot revoke credentials |
| 404 | ErrorResponse | No such provisioner |

### Example

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

## Revoke a credential

`DELETE /api/v1/agent-credentials/{id}`

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

Requires: `agent.credential.revoke`

Revokes one instance credential. It stops working immediately. The instance stays registered, and the agent and its other instances are not affected.

### Request

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

### Response

| Status | Body | Description |
| --- | --- | --- |
| 204 |  | The credential is revoked |
| 403 | ErrorResponse | You cannot revoke credentials |
| 404 | ErrorResponse | No such credential |

### Example

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

## Report the calling instance's tool catalogue

`PUT /api/v1/agent_instances/self/tool_catalog`

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

Self-service: acts on the caller's own account, no permission needed.

Replaces complete source snapshots in one discovery context. Failed sources retain their last successful definitions. The instance credential supplies organization and instance identity. No arguments or credentials belong in a report. This product endpoint is outside AAP. Maximum body size is 8 MiB.

### Request

Request body:

- `runtime` · string · required
- `discovery_context` · string · required: Stable opaque identifier for this local project or runtime context
- `complete` · boolean · required: Whether all configured sources in this context were enumerated
- `sources` · array<ToolCatalogSourceReport> · required
  - `source` · enum · required One of `builtin`, `mcp`.
  - `server` · string
  - `complete` · boolean · required
  - `error` · string
  - `tools` · array<ToolDefinition> · required
    - `name` · string · required: Exact tool name sent in approval requests
    - `display_name` · string
    - `description` · string
    - `input_schema` · object: Original JSON Schema, at most 256 KiB. Absence means unknown.
    - `annotations` · OnboardingToolAnnotations
      - `title` · string
      - `read_only_hint` · boolean
      - `destructive_hint` · boolean
      - `idempotent_hint` · boolean
      - `open_world_hint` · boolean

### Response

| Status | Body | Description |
| --- | --- | --- |
| 204 |  | Catalogue report recorded |
| 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. |

### Example

```bash
curl -X PUT "$WITHHUMAN_URL/api/v1/agent_instances/self/tool_catalog" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "runtime": "string",
  "discovery_context": "string",
  "complete": true,
  "sources": [
    {
      "source": "builtin",
      "server": "string",
      "complete": true,
      "error": "string",
      "tools": [
        {
          "name": "string",
          "display_name": "string",
          "description": "string",
          "input_schema": {},
          "annotations": {
            "title": "string",
            "read_only_hint": true,
            "destructive_hint": true,
            "idempotent_hint": true,
            "open_world_hint": true
          }
        }
      ]
    }
  ]
}'
```

## List known tool definitions

`GET /api/v1/tool_catalog`

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

Requires: `agent.read`

Returns definitions within the caller's agent scope. An edition's own tool source (the hosted gateway) is refreshed on read through a five-minute shared cache, with that edition's live grants applied. Failed refreshes retain the last successful definitions and mark their source stale. Catalogue information is advisory and never changes approval decisions.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agent_slug` | query | string | no |  |
| `q` | query | string | no |  |
| `source` | query | string | no | Only tools from this source: `builtin`, `mcp`, or a source an edition adds (the hosted gateway's `gateway`). |
| `status` | query | string | no |  |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |
| `refresh` | query | boolean | no | Bypass the edition's tool source discovery cache. Local tools require a CLI report. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | ToolCatalogPage | Known definitions and source discovery status |
| 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. |

Response body (200):

- `tools` · array<ToolCatalogEntryDefinition> · required
- `sources` · array<ToolCatalogSource> · required
  - `id` · uuid · required
  - `agent_slug` · string · required
  - `agent_instance_id` · uuid
  - `runtime` · string · required
  - `discovery_context` · string · required
  - `source` · string · required: `builtin`, `mcp`, or a source an edition adds (the hosted gateway's `gateway`)
  - `server` · string
  - `last_attempt_at` · date-time · required
  - `last_success_at` · date-time · required
  - `error` · string
- `total` · integer · required
- `next_offset` · integer · required

### Example

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

200 response

```json
{
  "tools": [
    {
      "name": "string",
      "display_name": "string",
      "description": "string",
      "input_schema": {},
      "annotations": {
        "title": "string",
        "read_only_hint": true,
        "destructive_hint": true,
        "idempotent_hint": true,
        "open_world_hint": true
      },
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "agent_slug": "string",
      "agent_instance_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "instance_name": "string",
      "discovery_context": "string",
      "runtime": "string",
      "source": "string",
      "server": "string",
      "available": true,
      "stale": true,
      "error": "string",
      "schema_fingerprint": "string",
      "last_seen_at": "2026-09-08T12:02:11Z",
      "last_attempt_at": "2026-09-08T12:02:11Z"
    }
  ],
  "sources": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "agent_slug": "string",
      "agent_instance_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "runtime": "string",
      "discovery_context": "string",
      "source": "string",
      "server": "string",
      "last_attempt_at": "2026-09-08T12:02:11Z",
      "last_success_at": "2026-09-08T12:02:11Z",
      "error": "string"
    }
  ],
  "total": 0,
  "next_offset": 1
}
```

## Read a tool definition

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

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

Requires: `agent.read`

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |
| `agent_slug` | query | string | no |  |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | ToolCatalogEntry | Definition and its discovery provenance |
| 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):

- `name` · string · required
- `display_name` · string
- `description` · string
- `input_schema` · object
- `annotations` · OnboardingToolAnnotations
  - `title` · string
  - `read_only_hint` · boolean
  - `destructive_hint` · boolean
  - `idempotent_hint` · boolean
  - `open_world_hint` · boolean
- `id` · uuid · required
- `agent_slug` · string · required
- `agent_instance_id` · uuid
- `instance_name` · string
- `discovery_context` · string · required
- `runtime` · string · required
- `source` · string · required: `builtin`, `mcp`, or a source an edition adds (the hosted gateway's `gateway`)
- `server` · string
- `available` · boolean · required
- `stale` · boolean · required
- `error` · string
- `schema_fingerprint` · string · required
- `last_seen_at` · date-time · required
- `last_attempt_at` · date-time · required

### Example

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

200 response

```json
{
  "name": "string",
  "display_name": "string",
  "description": "string",
  "input_schema": {},
  "annotations": {
    "title": "string",
    "read_only_hint": true,
    "destructive_hint": true,
    "idempotent_hint": true,
    "open_world_hint": true
  },
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "agent_slug": "string",
  "agent_instance_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "instance_name": "string",
  "discovery_context": "string",
  "runtime": "string",
  "source": "string",
  "server": "string",
  "available": true,
  "stale": true,
  "error": "string",
  "schema_fingerprint": "string",
  "last_seen_at": "2026-09-08T12:02:11Z",
  "last_attempt_at": "2026-09-08T12:02:11Z"
}
```

## Read an agent's gateway tool access

`GET /api/v1/agents/{slug}/tool-access`

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

Requires: `gateway.read`

Hosted edition only. The open edition answers 404.

Requires gateway.read and agent.read for the named agent. Unconfigured servers grant no access. All instances inherit these grants.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `slug` | path | string | yes |  |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | Saved grants, including no-access grants retained for revision checks |
| 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. |
| 501 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `grants` · array<AgentToolAccess> · required
  - `server_id` · uuid · required
  - `server_slug` · string · required
  - `mode` · enum · required One of `none`, `selected`, `all`.
  - `tools` · array<string> · required
  - `revision` · int64 · required

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/agents/{slug}/tool-access" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "grants": [
    {
      "server_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "server_slug": "string",
      "mode": "none",
      "tools": [
        "string"
      ],
      "revision": 1
    }
  ]
}
```

## Replace an agent's access to a server's tools

`PUT /api/v1/agents/{slug}/tool-access/{server_slug}`

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

Requires: `gateway.write`

Hosted edition only. The open edition answers 404.

Requires gateway.write and agent.read for the named agent. Grants exact tool names on this server, all current and future tools, or no tools. Supply the current revision, or zero for an unconfigured pair. A stale revision returns 412; an identical retry of the last write returns its committed result without another audit event. This version is the retry identity, so no Idempotency-Key is required. Archived agents and servers refuse edits. Inactive servers can be configured. The next MCP tool listing reflects changes; calls check live access before execution, including after approval. Already dispatched calls cannot be undone.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `slug` | path | string | yes |  |
| `server_slug` | path | string | yes |  |

Request body:

- `mode` · enum · required One of `none`, `selected`, `all`.
- `tools` · array<string> · required: Exact downstream tool names, meaningful only in selected mode. Empty for none or all; no wildcard matching.
- `revision` · int64 · required: Expected current version; zero for an unconfigured agent/server pair.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | AgentToolAccess | Saved grant with its new revision |
| 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. |
| 412 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 501 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `server_id` · uuid · required
- `server_slug` · string · required
- `mode` · enum · required One of `none`, `selected`, `all`.
- `tools` · array<string> · required
- `revision` · int64 · required

### Example

```bash
curl -X PUT "$WITHHUMAN_URL/api/v1/agents/{slug}/tool-access/{server_slug}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "mode": "none",
  "tools": [
    "string"
  ],
  "revision": 0
}'
```

200 response

```json
{
  "server_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "server_slug": "string",
  "mode": "none",
  "tools": [
    "string"
  ],
  "revision": 1
}
```
