# API reference

{/* // THE /v1 CONTRACT */}

Extrovert exposes a JSON REST API under `/v1`. The SDKs and MCP server use the same agent-plane
contract, so their operations are also available over HTTP.
**Interactive reference + machine-readable spec:** Browse every operation in the **[OpenAPI reference](https://docs.extrovert.dev/api/reference/)**. It is rendered directly from
  the canonical spec. The live API also serves the spec at
  [`/openapi.yaml`](https://api.extrovert.dev/openapi.yaml) and
  [`/openapi.json`](https://api.extrovert.dev/openapi.json) without authentication. Route parity tests
  verify that every documented operation exists and every public route is documented. Point client
  generators at either URL.

## Base URL

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

Set `EXTROVERT_API_BASE_URL` to override this URL in the SDK, MCP server, or your own client. Use
`mock` with the TypeScript SDK to run against local fixtures.

## Authentication

The API has two authentication planes. Permissions come from the key on the agent plane and from the signed-in
user plus the path on the admin plane.

- **Agent plane** (`/v1/...`, excluding `/v1/admin`) uses a **permissioned agent key** (`pk_agent_…`). The
  `customer_id`, **org, and project** are derived from the key, never from client input, so the API is
  tenant-scoped by construction. A broad key narrows to one project by **path**
  (`/v1/projects/{project_id}/...`), never a header or query.
- **Console / admin plane** (`/v1/admin/...`) uses a signed-in console session. Authority is derived from the
  identity's membership in the org/project addressed **in the path**
  (`/v1/admin/orgs/{org_id}/projects/{project_id}/...`). There are no permission headers.

```bash frame="terminal"
curl -H "Authorization: Bearer pk_agent_…" "$EXTROVERT_API_BASE_URL/v1/inboxes"
```

`POST /v1/enroll` and `POST /v1/agent/sign-up` are the unauthenticated bootstrap endpoints. See
[Authentication & keys](https://docs.extrovert.dev/quickstart/authentication/) and [Enrollment & identity](https://docs.extrovert.dev/api/enrollment/).

### Key tiers (agent plane)

An agent key carries a **ceiling tier** in its authenticated server record. The tier is the maximum
reach of the key. A call may narrow within it by path but never above it.

| Tier | Prefix | Reach | Created by |
|---|---|---|---|
| **org** | `pk_agent_org_…` | The key's org **and its descendant orgs** (the whole subtree). | Console / admin only; never self-served. |
| **project** | `pk_agent_…` | One bound project. This is the default. | Enrollment redeem + console. |
| **inbox** | `pk_agent_inbox_…` | One bound inbox in one project. | Console / admin only. |

Project keys use the bare `pk_agent_…` prefix. A key cannot issue a child key
with broader authority than its own.

### Addressing projects by path

The canonical agent path puts the project (and inbox) in the URL:

```
/v1/projects/{project_id}/inboxes/{inbox_id}
```

- `/v1/inboxes` (bare) is curl sugar that resolves to the **project key's bound project**. For an **org**
  key a bare list returns `400 breadth_required`; pick a breadth (see [pagination](#list-pagination--cursors)).
- `/v1/projects/-/inboxes` is the **org-wide wildcard**; an org key lists every inbox across its subtree.
  A non-org key on the wildcard is `403 forbidden_scope`.
- `/v1/projects/{other}/...` where `{other}` is outside the key's ceiling is `404 not_found` (never an
  existence oracle).

## Endpoints

The **[interactive OpenAPI reference](https://docs.extrovert.dev/api/reference/)** lists every operation with its request,
response, parameter, and authentication schemas. These curated pages explain the common workflows.

| Area | Curated page | What's there |
|---|---|---|
| **Identity & enrollment** | [Enrollment & identity](https://docs.extrovert.dev/api/enrollment/) | `enroll`, `sign-up`, `verify`, `/v1/auth/me` |
| **Inboxes** | [Inboxes](https://docs.extrovert.dev/api/inboxes/) | create, list, get, update, credentials, delete, and send |
| **Messages & threads** | [Messages & threads](https://docs.extrovert.dev/api/messages-and-threads/) | list / get / raw / search / mark-read / reply / forward / threads / attachments |
| **wait_for_email** | [wait_for_email](https://docs.extrovert.dev/api/wait/) | `POST /v1/inboxes/{inbox_id}/wait` (+ legacy `POST /v1/wait`) |
| **Webhooks** | [Webhooks](https://docs.extrovert.dev/api/webhooks/) | CRUD + HMAC delivery + signature verification |
| **Review Loop** | [Agent contract](https://docs.extrovert.dev/review-loop/agent-contract/) | reviews, events, decisions, categories, and writing rules |
| **Domains** | [Interactive reference](https://docs.extrovert.dev/api/reference/) | onboard, verify, list, inspect, and offboard domains |
| **Commerce** | [Purchase approvals](https://docs.extrovert.dev/concepts/purchase-approvals-for-agents/) | quote domains and create, inspect, cancel, or poll human-controlled purchase and plan requests |
| **Suppressions** | [Unsubscribes & suppression](https://docs.extrovert.dev/concepts/unsubscribes-and-suppression/) | precheck, list, and revoke recipient opt-outs |
| **Jobs** | [Interactive reference](https://docs.extrovert.dev/api/reference/) | poll asynchronous domain offboarding |
| **Errors** | [Errors](https://docs.extrovert.dev/api/errors/) | status codes and the problem+json envelope |

The `/v1/admin/*` console plane is browser-authenticated and appears in the
[interactive reference](https://docs.extrovert.dev/api/reference/) and the [OpenAPI spec](https://api.extrovert.dev/openapi.yaml).

## Conventions

| Topic | Rule |
|---|---|
| **Content type** | Requests are `application/json`; error responses are `application/problem+json`. |
| **IDs** | Prefixed and **opaque**: `msg_…`, `thread_…`, `agent_…`, `wh_…`, and the inbox id (`pmbx_…`). Treat every id as an opaque string. An inbox's **canonical key** is its opaque `id` (`{inbox_id}`); its email **address** is accepted in the same path slot as a within-project alias (URL-encode it: `agent7%40extrovertmail.com`). |
| **Resource envelope** | Newer resources include an `object` discriminator, scope ids, and timestamps. Legacy message, thread, and attachment shapes vary; use the schema for the resource you are reading. |
| **List pagination** | Cursor-paginated collections return the **one list envelope** `{ object: "list", data, has_more, next_cursor }`. `next_cursor` is **opaque**; pass it back verbatim as `?cursor=`. Use `?limit=` (1–100, default 50). See [List pagination & cursors](#list-pagination--cursors). |
| **Idempotency** | Mutating operations that document `Idempotency-Key` replay the original response for the same key and body. Reusing a key with a different body returns `409 idempotency_conflict`. The key is scoped per tenant, agent, and project. |
| **Versioning** | Send a dated `Extrovert-Version` header (e.g. `2026-06-23`) to pin a snapshot; omit it for the latest. A request-time shim transforms older pinned versions to the current shape. |
| **Expansion** | `?include=` opt-in expands relations (per-resource allowlist, depth ≤ 2); each join re-applies the same ceiling/ownership filter. A disallowed or too-deep path is `400 bad_request`. |
| **Timestamps** | Resource timestamps are ISO 8601 UTC (`2026-06-18T18:04:11Z`); message `date` is the original RFC5322 header value. |
| **Errors** | RFC-9457 `application/problem+json` with a closed machine `code` enum. See [Errors](https://docs.extrovert.dev/api/errors/). |

## List pagination & cursors

Cursor-paginated collections (e.g. `/v1/projects/{project_id}/inboxes`) return the one envelope:

```json
{
  "object": "list",
  "data": [ /* … resource objects … */ ],
  "has_more": true,
  "next_cursor": "eyJvIjo1MCwidiI6MX0"
}
```

- `next_cursor` is an **opaque** token. Do not parse it. Pass it back as `?cursor=` to fetch
  the next page. It is `null` when there are no more rows.
- `?limit=` bounds the page size to `[1, 100]` (default 50).
- **Breadth follows the key tier.** A project/inbox key on the bare `/v1/inboxes` (or
  `/v1/projects/{own}/inboxes`) lists that project's inboxes. An **org** key must pick a breadth: the
  bare list returns `400 breadth_required` (the problem body names the next call), and
  `/v1/projects/-/inboxes` returns the org-wide list (rows labeled with their `org_id` + `project_id`).
**Two pagination surfaces:** The cursor envelope above is the agent surface. Some legacy message/thread reads and the console/admin
  plane still return `{ items, total, next_cursor? }` with an offset-style `next_cursor` (pass back as
  `?cursor=`/`?offset=`) and the admin `next_page`/`?page=` token. New agent-plane collections use the
  opaque-cursor `List` envelope.

## Rate limits

Per-key limits are published and designed not to punish fan-out across many inboxes. A `429` carries a
`Retry-After` header. See [Rate limits & quotas](https://docs.extrovert.dev/operating/limits/). fan-out friendly

## Per-endpoint reference

- [Interactive OpenAPI reference](https://docs.extrovert.dev/api/reference/): every operation generated from the spec
- [Enrollment & identity](https://docs.extrovert.dev/api/enrollment/): enroll, sign-up, verify, and `/v1/auth/me`
- [Inboxes](https://docs.extrovert.dev/api/inboxes/): create, list, get, update, credentials, delete, and send
- [Messages & threads](https://docs.extrovert.dev/api/messages-and-threads/): list, search, read, mark, reply, forward, attachments, and threads
- [wait_for_email](https://docs.extrovert.dev/api/wait/): `POST /v1/inboxes/{inbox_id}/wait`
- [Webhooks](https://docs.extrovert.dev/api/webhooks/): CRUD and signature verification
- [Errors](https://docs.extrovert.dev/api/errors/): status codes and problem+json `code`s