# API · Enrollment & identity

{/* // POST /v1/enroll, /v1/agent/sign-up, /v1/agent/verify, GET /v1/auth/me */}

Use an enrollment token to create an agent key. Free self-signup is also part of the API, but it is
currently paused. Any valid agent key can call `/v1/auth/me` to inspect its authority.

## Redeem an enrollment token: `POST /v1/enroll`

Redeem an enrollment token (`pk_enroll_…`) issued from the console for a permissioned agent key
(`pk_agent_…`). The enrollment token in the request body is the credential.

### Request

```json
{
  "token": "pk_enroll_…",        // required
  "agent_handle": "support-bot", // optional; idempotent binding key
  "agent_name": "Support Bot"    // optional
}
```

| Field | Type | Notes |
|---|---|---|
| `token` | string | The `pk_enroll_…` enrollment token. Validated against its stored hash. |
| `agent_handle` | string? | Client-chosen agent identifier. Same handle → same agent (idempotent). |
| `agent_name` | string? | Friendly display name for the agent. |

```bash frame="terminal"
curl -sS -X POST "$EXTROVERT_API_BASE_URL/v1/enroll" \
  -H "Content-Type: application/json" \
  -d '{ "token": "pk_enroll_…", "agent_handle": "support-bot" }'
```

### Response `201`

```json
{
  "agent_id": "agent_Q1x",
  "agent_key": "pk_agent_pkey_7Hq2…",
  "scopes": ["mailbox:create", "mailbox:read", "mailbox:send"],
  "org_id": "org_…",
  "project_id": "prj_…"
}
```

Enrollment creates a **project-tier** key (`pk_agent_…`) bound to the token's **org and project**
(`org_id` and `project_id`). The agent cannot change either value. Every inbox, webhook, and rule it
creates belongs to that project. Org- and inbox-tier keys (`pk_agent_org_…` and `pk_agent_inbox_…`)
use explicit tier prefixes and are issued only from the console/admin plane. See
[Key tiers](https://docs.extrovert.dev/api/overview/#key-tiers-agent-plane).
**agent_key is shown once:** The full `agent_key` is returned only in this response. Store it immediately. If you lose it, redeem
  again with the same `agent_handle` to issue a fresh key for the same agent, subject to the enrollment
  token's quota and expiry).

### Errors

The redeem is rejected when the token is unknown, revoked, expired, or exhausted. See
[Errors](https://docs.extrovert.dev/api/errors/) for the envelope and status codes.

### Idempotency

Redeeming with the same `agent_handle` returns the same agent. It does not create a duplicate agent or
consume an inbox slot. The handle is unique within the token's project, so the same handle in another
project identifies a different agent.

## Self-signup: `POST /v1/agent/sign-up`
**Free signups are temporarily paused:** This endpoint and pending signup-code verification currently return `403 signup_disabled` without
  creating or elevating account state. Enrollment-token redemption and invitation acceptance remain
  available. Read `GET /v1/signup-status` for the current database-backed availability value.

When free signup is enabled, this call creates a free account and first inbox, returns a
verification-only agent key, and emails a code to `human_email`. Pass that code to
[`/v1/agent/verify`](#verify-a-signup-code-post-v1agentverify) to receive a full-permission key.
The inbox uses `free.extrovertmail.com`.

### Request

```json
{
  "human_email": "you@example.com", // required; where the code is sent
  "username": "support"             // optional; local part for the first inbox
}
```

```bash frame="terminal"
curl -sS -X POST "$EXTROVERT_API_BASE_URL/v1/agent/sign-up" \
  -H "Content-Type: application/json" \
  -d '{ "human_email": "you@example.com" }'
```

### Response `201`

```json
{
  "customer_id": "cust_8Vd",
  "agent_id": "agent_Q1x",
  "agent_key": "pk_agent_pkey_7Hq2…",
  "key_prefix": "pk_agent_pkey_7Hq2",
  "scopes": ["signup:verify"],
  "address": "support@free.extrovertmail.com",
  "verified": false,
  "otp_sent_to": "you@example.com",
  "otp_expires_at": "2026-06-18T18:15:00Z",
  "message": "A verification code was sent to your email. Call POST /v1/agent/verify with it to unlock full permissions."
}
```
**The OTP is never in the body:** The verification code goes only to `human_email`. The signup response tells you where and when it was
  sent, not the code itself.

The unverified key can call only the verification endpoint. It cannot read or send mail, create
another inbox, or export IMAP and SMTP credentials.

If the verification email cannot be sent, the API returns `502 otp_send_failed`. Re-call sign-up with
the same `human_email` to rotate the limited key and send a fresh code.

## Verify a signup code: `POST /v1/agent/verify`

Confirm the emailed code and exchange the `signup:verify` key for a **full-permission** key. Send the
verification-only key as the bearer token.

### Request

```json
{ "otp": "492013" }
```

```bash frame="terminal"
curl -sS -X POST "$EXTROVERT_API_BASE_URL/v1/agent/verify" \
  -H "Authorization: Bearer $EXTROVERT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "otp": "492013" }'
```

### Response `200`

```json
{
  "agent_id": "agent_Q1x",
  "agent_key": "pk_agent_pkey_F9zK…",
  "key_prefix": "pk_agent_pkey_F9zK",
  "scopes": ["mailbox:create", "mailbox:read", "mailbox:send"],
  "address": "support@free.extrovertmail.com",
  "verified": true,
  "message": "Verified. The bootstrap key was revoked and the inbox is ready.",
  "mailbox_quickstart": {
    "inbox": "support@free.extrovertmail.com",
    "list_mail": {
      "tool": "read_messages",
      "arguments": { "inbox": "support@free.extrovertmail.com" }
    },
    "read_message": {
      "tool": "get_message",
      "arguments": { "id": "<message_id from read_messages>", "format": "text", "variant": "extracted" }
    },
    "wait_for_mail": {
      "tool": "wait_for_email",
      "arguments": { "inbox": "support@free.extrovertmail.com" }
    }
  }
}
```

The bootstrap key is revoked atomically when verification succeeds. `address` repeats the ready inbox,
and `mailbox_quickstart` provides copy-ready MCP calls for listing, reading, and waiting for mail.
Fresh signups may also return an `org_claim_token` for the human console handoff. The signed-in user
calls `GET /v1/admin/me`, selects the matching item from `pending_signup_claims`, then calls
`POST /v1/admin/signup-claims/{claim_id}/accept`. An invalid or expired code returns
`400 otp_invalid`.

## Free-signup availability: `GET /v1/signup-status`

This unauthenticated, non-cacheable endpoint reports whether free agent signup and first-root console
bootstrap are available. Treat `503 signup_gate_unavailable`, a timeout, or a malformed response as
disabled.

```json title="response 200"
{ "free_signups_enabled": false }
```

## Inspect the current key: `GET /v1/auth/me`

Return the principal behind the agent key, including its fixed org and project. This endpoint requires
only a valid key, so an agent can always inspect its own authority.

```bash frame="terminal"
curl -sS "$EXTROVERT_API_BASE_URL/v1/auth/me" \
  -H "Authorization: Bearer $EXTROVERT_API_KEY"
```

### Response `200`

```json
{
  "customer_id": "cust_8Vd",
  "org_id": "org_…",
  "project_id": "prj_…",
  "tier": "project",
  "agent_id": "agent_Q1x",
  "key_id": "key_5dR",
  "scopes": ["mailbox:create", "mailbox:read", "mailbox:send"]
}
```

`tier` is the key's ceiling (`org` / `project` / `inbox`); `org_id` and `project_id` are resolved from
the stored key and cannot be changed by the caller. There is no mutable active-project selector on the
agent plane; `whoami` is how an agent reads its authority. To act in a project, name it in the **path**
(`/v1/projects/{project_id}/...`): a project key may name only its bound project (any other → `404`); an
org key may name any project in its subtree (or `-` for the org-wide wildcard).

## Next

- [Enrollment tokens & scopes](https://docs.extrovert.dev/concepts/enrollment-tokens/) explains the capability model.
- [Inboxes](https://docs.extrovert.dev/api/inboxes/) covers the first call to make with the new agent key.