# Agent enrollment

Connect an agent from a developer machine. The agent asks for a code, a person approves it in the browser, and the agent exchanges the code for a credential. A withHuman extension, not part of the protocol.

Enrollment is for an agent that runs on a developer machine, where nobody
can hand it a credential in advance. The machine asks for a short code. A
person approves that code in the browser, and the machine exchanges it for
a credential of its own. No secret is ever copied by hand.

`withhuman agent install <runtime>` runs this flow for you. Use these endpoints to build the
same flow into your own tool. Neither endpoint needs authentication.

This is a withHuman extension. It is not part of the Agent Approval
Protocol, so other providers may not offer it.

How it works:

1. [Start an enrollment](#beginAgentEnrollment) with the runtime and instance name. Optionally set
   `requested_agent_name` to require an existing parent agent. The response carries an `enrollment_code` for the
   machine, a `user_code` for the person, and a `verification_uri`.
2. Show the person the verification URI and the user code. They sign in,
   enter the code, and select an existing agent or create one. This needs the
   `agent.credential.issue` permission, which owners and admins hold.
   Creating an agent also needs `agent.write`. Authorizing attaches the
   instance to the selected agent. A requested agent name fixes the target
   and never creates an agent automatically.
3. In the meantime, [exchange the enrollment code](#exchangeAgentEnrollment)
   every few seconds. It returns `404` until the person has authorized the
   code. Then it returns the selected agent, the new instance and its credential. Store the
   credential's token; it is not shown again.

Both codes expire ten minutes after the enrollment starts. The response to
the first call says exactly when. If that time passes, start over.

## Start an enrollment

`POST /auth/agent_enrollments`

Auth: No authentication

Starts an enrollment and returns two codes. The `enrollment_code` stays on the machine. The `user_code` is for a person to enter at the `verification_uri`. Both expire at `expires_at`, ten minutes after this call. The person selects an existing agent or creates one in the browser, then authorizes the instance. An optional requested agent name fixes the target within the organization they sign into.

### Request

Request body:

- `runtime` · string · required: The runtime being installed, independent of its parent agent.
- `requested_agent_slug` · string: Optional slug of an existing agent. The browser must authorize this target; it cannot create or substitute another agent.
- `instance_name` · string · required: A name for this instance, unique within the agent. The CLI defaults to the machine's hostname plus a unique suffix. Reviewers see it next to the agent's name.
- `instance_metadata` · object: Any JSON object to store with the instance, such as the hostname or operating system.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | EnrollmentCode | The codes and where to use them |
| 400 |  | A required field is missing or blank |

Response body (201):

- `enrollment_code` · string · required: The secret the machine keeps. Send it to the exchange endpoint. Never show it to the person.
- `user_code` · string · required: The short code the person enters in the browser. Three letters, a hyphen, and four letters or digits. Case does not matter.
- `verification_uri` · uri · required: The page where the person enters the user code.
- `expires_at` · date-time · required: When both codes stop working. Start a new enrollment after this.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/auth/agent_enrollments" \
  -H "Content-Type: application/json" \
  -d '{
  "runtime": "claude-code",
  "requested_agent_slug": "codex",
  "instance_name": "eces-macbook",
  "instance_metadata": {
    "hostname": "eces-macbook",
    "os": "darwin"
  }
}'
```

201 response

```json
{
  "enrollment_code": "whe_7c1f2a9e-4b3d-4f2e-9a1c-2d6e8b5f0a11_3f6b9c1d0e7a4b2c",
  "user_code": "K7M-3PQ2",
  "verification_uri": "https://app.withhuman.ai/connect/authorize",
  "expires_at": "2026-09-08T12:02:11Z"
}
```

## Exchange an enrollment code

`POST /auth/agent_enrollments/token`

Auth: No authentication

Exchanges an authorized enrollment code for the new instance and its credential. Call this every few seconds after starting an enrollment. It returns `404` until a person has authorized the user code. Once it succeeds, the code is used up and a second call returns `404` as well. The credential's token appears only in this response, so store it right away.

### Request

Request body:

- `enrollment_code` · string · required: The `enrollment_code` returned when the enrollment started.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | AgentEnrollmentCredential | The instance and its credential. The credential's token appears only in this response. |
| 401 |  | The enrollment code is not in the expected format |
| 403 |  | The agent or the instance has been disabled since it was authorized |
| 404 |  | The code has not been authorized yet, has expired, or has already been exchanged |

Response body (200):

- `agent` · Agent · required: An agent identity. The slug identifies it everywhere: URLs, role scopes, pipeline scopes, audit data and conditions. The name is a label for people.
  - `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`.
- `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/auth/agent_enrollments/token" \
  -H "Content-Type: application/json" \
  -d '{
  "enrollment_code": "whe_7c1f2a9e-4b3d-4f2e-9a1c-2d6e8b5f0a11_3f6b9c1d0e7a4b2c"
}'
```

200 response

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