# Authentication and keys

Extrovert supports three normal authentication paths:

| Path | Use it when | Result |
|---|---|---|
| Hosted MCP OAuth | A person already has a console account and the MCP client supports remote OAuth | A refreshable grant for the person's default project and a limited permission set |
| Enrollment key | Setup needs to create the agent | A new or existing agent identity plus a project-bound agent key |
| API key | The agent already exists | A project-bound key for that agent |

Do not give an agent a human session or an organization-wide administrative credential.

## Enrollment keys and API keys are different

The console separates the two under **Credentials**:

- **Enrollment keys** bootstrap agents. They may be reusable or single-use, expire independently, and
  can limit permissions, allowed domains, lifetime inbox creation, and daily sending.
- **API keys** connect software to an existing active agent. They do not create an agent.

Both raw secrets are shown once. Store them in a secret manager or the packaged MCP credential store.

## Redeem an enrollment key

Redemption is idempotent on `agent_handle`. Reusing the same stable handle returns the same agent
instead of creating duplicates.

```bash frame="terminal"
export EXTROVERT_API_BASE_URL="https://api.extrovert.dev"
export EXTROVERT_ENROLLMENT_KEY="pk_enroll_…"

curl -sS -X POST "$EXTROVERT_API_BASE_URL/v1/enroll" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: enroll-support-agent" \
  -d "{\"token\":\"$EXTROVERT_ENROLLMENT_KEY\",\"agent_handle\":\"support-agent\"}"
```

  ```ts title="enroll.ts"
import { Extrovert } from "@extrovert.dev/sdk";

const bootstrap = new Extrovert({ apiKey: process.env.EXTROVERT_ENROLLMENT_KEY! });
const { client, enrollment } = await bootstrap.enrolled({
  token: process.env.EXTROVERT_ENROLLMENT_KEY!,
  agent_handle: "support-agent",
  client_id: "enroll-support-agent",
});

console.log(enrollment.agent_key); // shown once
```

  ```json title="redeem_enrollment"
{
  "name": "redeem_enrollment",
  "arguments": {
    "enrollment_token": "pk_enroll_…",
    "agent_handle": "support-agent"
  }
}
```

  Use the returned agent key as a bearer token:

```http
Authorization: Bearer pk_agent_…
```

## Key reach is fixed

An agent key has a ceiling that cannot be widened after issuance.

| Ceiling | Prefix | Reach |
|---|---|---|
| Organization | `pk_agent_org_…` | The bound organization and allowed descendants; console or admin issuance only |
| Project | `pk_agent_…` | One project; this is the normal enrollment and self-signup result |
| Inbox | `pk_agent_inbox_…` | One inbox in one project; console or admin issuance only |

`pk_agent_proj_…` remains accepted as a compatibility form for project keys. A project id in a
request is an assertion, not a selector. If it does not match the key's project, the request fails.

Call `GET /v1/auth/me`, `client.whoami()`, or `whoami` after connecting. Record the fixed `org_id`,
`project_id`, agent id, key id, and permissions before doing work.

## Permissions

The product calls the resource an **inbox**. Existing `mailbox:*` strings remain unchanged because
they are part of the public wire contract.

| Permission | Allows |
|---|---|
| `mailbox:create` | Create inboxes and perform owning-agent lifecycle cleanup |
| `mailbox:read` | Read inboxes, messages, threads, rules, review events, suppressions, and contact lists |
| `mailbox:send` | Submit send, reply, forward, review chat, and revision requests |
| `mailbox:delete` | Permanently delete inboxes |
| `mailbox:credentials` | Export portable IMAP and SMTP credentials on an eligible paid plan |
| `mailbox:quota` | Change an inbox's rolling 24-hour recipient limit |
| `webhook:write` | Create, update, and delete webhooks; older keys may use the documented read fallback |
| `domain:manage` | Onboard, verify, inspect, and offboard customer-controlled domains |
| `commerce:request` | Quote and request a domain purchase or plan change; never approve or authorize spending |
| `review:act` | Act as a linked Review Loop reviewer; the link is also required |
| `signup:verify` | Verify a self-signup code only |

`domain:purchase` is a legacy compatibility permission. New agent credentials should use
`commerce:request` for human-controlled purchase requests.

## Hosted MCP OAuth

Connect an OAuth-capable client to:

```text
https://mcp.extrovert.dev/mcp
```

The client discovers the authorization server, opens browser sign-in and consent, then stores and
refreshes the grant. No Extrovert key belongs in the client configuration.

The grant is pinned to the signed-in person's default project. Owners, admins, and members receive
inbox create, read, send, webhook, and commerce request permissions. Viewers and billing members
receive read and commerce request permissions. Hosted MCP can also accept a limited `pk_agent_…`
bearer credential. It rejects enrollment keys.

## Free self-signup
**Free self-signup is paused:** Check `GET /v1/signup-status` before presenting self-signup. While the gate is disabled,
  `POST /v1/agent/sign-up` and pending verification return `403 signup_disabled` and create or
  elevate nothing. Enrollment keys, invitations, and existing accounts continue to work.

When enabled, the flow is:

1. `POST /v1/agent/sign-up` creates the agent and an `@free.extrovertmail.com` inbox, emails a
   verification code, and returns a temporary project key with only `signup:verify`.

2. The temporary key can only verify the OTP. It cannot create, read, send, or export credentials. It
   expires with the code.

3. `POST /v1/agent/verify` atomically revokes the temporary key and returns a new durable key, the
   activated inbox address, and `mailbox_quickstart` calls for `read_messages`, `get_message`, and
   `wait_for_email`.

4. The person signs in to the console with the same verified email. Extrovert presents the matching
   agent-created account and asks the person to claim it. Accepting the claim grants owner access.

The verification code appears only in email. It is never included in the signup response. If delivery
fails with `otp_send_failed`, repeat signup with the same normalized email to rotate the temporary key
and send a new code.

## Storage and revocation

The packaged local MCP server stores the durable agent key in a permission-restricted credential file:

- Linux and macOS: `~/.config/extrovert/credentials.json` or
  `$XDG_CONFIG_HOME/extrovert/credentials.json`
- Windows: `%APPDATA%\Extrovert\credentials.json`

`EXTROVERT_API_KEY` is an explicit override. Keep secrets out of source files, shell history, logs,
screenshots, and support bundles. Revoke a key that may have been exposed.

Revoking an enrollment key prevents future redemption. It does not revoke agent keys already issued
from it. Revoke those keys or disable the agent separately when needed.

## Common failures

| Status or code | Meaning | Next action |
|---|---|---|
| `401` | The credential is missing, expired, revoked, or invalid | Replace or reconfigure the exact credential |
| `403 forbidden_scope` | The key is valid but lacks a required permission | Issue a narrower-purpose key with the required permission |
| `403 signup_disabled` | Free self-signup or pending verification is paused | Use an enrollment key or wait for the gate to reopen |
| `404` | The resource is outside the key's ownership or project reach, or does not exist | Verify `whoami`, project binding, and the opaque resource id |
| `409 quota_exceeded` or `403 quota_exceeded` | A lifetime or plan limit prevents creation | Inspect the returned problem details and issue a new key or change plan capacity as appropriate |

## Next

- [Zero to first email](https://docs.extrovert.dev/quickstart/zero-to-first-email/)
- [Enrollment keys and permissions](https://docs.extrovert.dev/concepts/enrollment-tokens/)
- [MCP client configuration](https://docs.extrovert.dev/mcp/client-configuration/)
- [API errors](https://docs.extrovert.dev/api/errors/)