> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dairo.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Scopes

> How API key scopes work: the read and write scope per resource, convenience bundles, and how child keys stay within their parent's access.

Every API key carries a set of scopes that decide what it can do. Give each key the
narrowest set its job needs: a sender that can only send, a webhook worker that can only
read, an admin key that can do everything.

Scopes follow one pattern. Every resource has its own `read` and `write` pair, a read is
never gated behind a write, and no scope is borrowed across resources: listing inboxes
needs `inboxes:read`, creating one needs `inboxes:write`.

Check what any key can do with `whoami`:

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl https://api.dairo.app/v1/whoami \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const me = await dairo.whoami();
  console.log(me.apiKey.scopes); // ["messages:send", "messages:read", "inboxes:write", ...]
  ```

  ```python title="Python" theme={null}
  me = dairo.whoami()
  print(me.api_key.scopes)
  ```

  ```bash title="CLI" theme={null}
  dairo whoami
  ```
</CodeGroup>

## The scope matrix

Scopes use a `resource:action` grammar: a `read`/`write` pair for each resource you can
change, plus a read-only scope for account data.

| Resource                                 | Read scope        | Write scope        |
| ---------------------------------------- | ----------------- | ------------------ |
| Mailbox (messages, threads, attachments) | `messages:read`   | `messages:send`    |
| Letters (physical mail)                  | `letters:read`    | `letters:send`     |
| Dairo Phone (calls and numbers)          | `phone:read`      | `phone:write`      |
| Inboxes                                  | `inboxes:read`    | `inboxes:write`    |
| Contacts                                 | `contacts:read`   | `contacts:write`   |
| Domains                                  | `domains:read`    | `domains:write`    |
| Webhooks                                 | `webhooks:read`   | `webhooks:write`   |
| API keys                                 | `keys:read`       | `keys:write`       |
| Templates                                | `templates:read`  | `templates:write`  |
| Audiences                                | `audiences:read`  | `audiences:write`  |
| Storage (buckets and objects)            | `buckets:read`    | `buckets:write`    |
| Budgets                                  | `budgets:read`    | `budgets:write`    |
| Agents                                   | `agents:read`     | `agents:write`     |
| Events                                   | `events:read`     | `events:write`     |
| Compliance (erasure jobs)                | `compliance:read` | `compliance:write` |
| Account and audit logs                   | `account:read`    | `account:write`    |

`messages:read` covers the mailbox only (messages, threads, attachments) and
`messages:send` sends messages only; everything else uses its own resource scope. Dairo
Phone adds a third action scope on top of its read/write pair: `phone:call` places and
ends live calls, separate from `phone:read` (list and inspect) and `phone:write` (buy,
bind, and release numbers).

## What each scope unlocks

| Scope              | Endpoints it unlocks                                                                                                                                                                                                                         |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messages:read`    | `GET /v1/messages` (incl. `?direction=outbound` and `?channel=a2a`), `GET /v1/messages/{id}`, `GET /v1/messages/{id}/events`, `POST /v1/messages/batch-delete`, `GET /v1/threads[/{id}]`, `GET /v1/attachments/{id}[/url\|/link\|/download]` |
| `messages:send`    | `POST /v1/messages`, `POST /v1/messages/{id}/cancel`, `POST /v1/messages/{id}/{edit\|react\|unreact}`, `POST /v1/audiences/{id}/send`                                                                                                        |
| `letters:read`     | `GET /v1/letters[/{id}][/events]`, `POST /v1/letters/price`, `GET /v1/letters/templates[/{id}]`, `GET /v1/letters/batches/{id}`                                                                                                              |
| `letters:send`     | `POST /v1/letters`, `POST /v1/letters/{id}/cancel`, `POST /v1/letters/batches`, `POST`/`PATCH /v1/letters/templates[/{id}]`, `POST /v1/letters/templates/{id}/preview`                                                                       |
| `phone:read`       | `GET /v1/phone/numbers[/available\|/{id}]`, `GET /v1/phone/calls[/{id}][/transcript\|/recording]`                                                                                                                                            |
| `phone:write`      | `POST /v1/phone/numbers`, `PATCH`/`DELETE /v1/phone/numbers/{id}`                                                                                                                                                                            |
| `phone:call`       | `POST /v1/phone/calls`, `POST /v1/phone/calls/{id}/hangup`                                                                                                                                                                                   |
| `inboxes:read`     | `GET /v1/inboxes`, `GET /v1/inboxes/{id}/schema`, `GET /v1/inboxes/{id}/verification-waits[/{waitId}]`                                                                                                                                       |
| `inboxes:write`    | `POST /v1/inboxes`, `PATCH`/`DELETE /v1/inboxes/{id}`, `PUT`/`DELETE /v1/inboxes/{id}/schema`, `POST`/`DELETE` on `verification-waits`, and Slack app management (`/v1/slack/*`)                                                             |
| `contacts:read`    | `GET /v1/contacts[/{id}]`, `GET /v1/contacts/me`, `GET /v1/contacts/{id}/messages`                                                                                                                                                           |
| `contacts:write`   | `POST`/`PATCH`/`DELETE /v1/contacts[/{id}]`, `POST /v1/contacts/{id}/handles`, `DELETE /v1/contacts/{id}/handles/{handleId}`                                                                                                                 |
| `domains:read`     | `GET /v1/domains`                                                                                                                                                                                                                            |
| `domains:write`    | `POST /v1/domains`, `PATCH`/`DELETE /v1/domains/{domain}`, `POST /v1/domains/{domain}/verify`                                                                                                                                                |
| `webhooks:read`    | `GET /v1/webhooks`, `GET /v1/webhooks/{id}/deliveries`                                                                                                                                                                                       |
| `webhooks:write`   | `POST /v1/webhooks`, `PATCH`/`DELETE /v1/webhooks/{id}`, `POST /v1/webhooks/{id}/{ping\|pause\|resume}`, `POST /v1/webhooks/{id}/deliveries/{deliveryId}/redrive`                                                                            |
| `keys:read`        | `GET /v1/api-keys`                                                                                                                                                                                                                           |
| `keys:write`       | `POST /v1/api-keys`, `PATCH`/`DELETE /v1/api-keys/{id}` (also required to mint child keys)                                                                                                                                                   |
| `templates:read`   | `GET /v1/templates[/{id}]`, `GET /v1/templates/{id}/versions[/{version}]`                                                                                                                                                                    |
| `templates:write`  | `POST`/`PATCH`/`DELETE /v1/templates[/{id}]`, `POST /v1/templates/{id}/versions`                                                                                                                                                             |
| `audiences:read`   | `GET /v1/audiences[/{id}]`                                                                                                                                                                                                                   |
| `audiences:write`  | `POST`/`DELETE /v1/audiences[/{id}]`, `POST /v1/audiences/{id}/members` (membership only; **sending** a broadcast needs `messages:send`)                                                                                                     |
| `buckets:read`     | `GET /v1/buckets[/{id}]`, `GET /v1/buckets/{id}/objects[/{objectId}][/download]`, object share-links, `GET /v1/share-links/{id}/opens`                                                                                                       |
| `buckets:write`    | `POST`/`PATCH`/`DELETE /v1/buckets[/{id}]`, object create/finalize/delete/batch-delete, per-object and bundle share links, `DELETE /v1/share-links/{id}`                                                                                     |
| `budgets:read`     | `GET /v1/budgets[/{scope}]`                                                                                                                                                                                                                  |
| `budgets:write`    | `PUT /v1/budgets`, `DELETE /v1/budgets/{scope}`                                                                                                                                                                                              |
| `agents:read`      | `GET /v1/agents[/{id}]`, `GET /v1/agents/reputation`, `GET /v1/agents/{id}/reputation`                                                                                                                                                       |
| `agents:write`     | `POST /v1/agents`, `DELETE /v1/agents/{id}`, `POST /v1/agents/{id}/bindings`, `DELETE /v1/agents/{id}/reputation`                                                                                                                            |
| `events:read`      | `GET /v1/events` (incl. long-poll and tail)                                                                                                                                                                                                  |
| `events:write`     | `POST /v1/events/replay`                                                                                                                                                                                                                     |
| `account:read`     | `GET /v1/audit-logs`, `GET /v1/audit-logs/export`, `GET /v1/account/residency`, `GET /v1/notifications/preferences`                                                                                                                          |
| `account:write`    | `PATCH /v1/notifications/preferences`                                                                                                                                                                                                        |
| `compliance:read`  | `GET /v1/erasure-jobs[/{id}]`                                                                                                                                                                                                                |
| `compliance:write` | `POST /v1/erasure-jobs`                                                                                                                                                                                                                      |

<Note>
  `GET /v1/whoami` and `GET /v1/telegram/voices` need no specific scope: any valid key
  works. Agent verification and JWKS (`GET /v1/agents/verify`, `GET /v1/agents/jwks`, and
  the `/.well-known/dairo-jwks.json` alias) are public and need no key at all.
</Note>

<Note>
  Scopes gate the MCP server and CLI too. The [MCP server](/agent-first/mcp-server)
  authenticates with the same key, so a tool call needs the same scope as the REST endpoint
  behind it: `send_message` needs `messages:send`, `list_contacts` and `manage_contacts`
  need `contacts:read` and `contacts:write`, and so on. A missing scope comes back as
  [`scope_missing`](/concepts/errors) with the exact scope named, so an agent can tell
  whether it needs a broader key.
</Note>

## Bundles for a quick start

If you would rather not enumerate fine-grained scopes, request a bundle. A bundle expands
to its member scopes when the key is created, so `whoami` reports the expanded set.

| Bundle     | Expands to                                |
| ---------- | ----------------------------------------- |
| `messages` | `messages:read`, `messages:send`          |
| `letters`  | `letters:read`, `letters:send`            |
| `phone`    | `phone:read`, `phone:write`, `phone:call` |
| `admin`    | every scope in the matrix above           |

<CodeGroup>
  ```ts title="TypeScript" theme={null}
  // A send-and-read mailbox key from the `messages` bundle.
  const key = await dairo.apiKeys.create({
    name: "mailer",
    scopes: ["messages"],
  });
  // key.scopes === ["messages:read", "messages:send"]
  ```

  ```python title="Python" theme={null}
  key = dairo.api_keys.create(name="mailer", scopes=["messages"])
  print(key.scopes)  # ["messages:read", "messages:send"]
  ```

  ```bash title="cURL" theme={null}
  curl -X POST https://api.dairo.app/v1/api-keys \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name":"mailer","scopes":["messages"]}'
  ```
</CodeGroup>

<Warning>
  `admin` grants everything, including `keys:write` (minting and revoking other keys) and
  `compliance:write` (data erasure). Reserve it for trusted automation; use a narrow scope
  list everywhere else.
</Warning>

## Least privilege in practice

Give a key the narrowest set it actually needs:

* A webhook receiver that only reads inbound messages needs `messages:read`.
* A transactional sender needs `messages:send`, plus `messages:read` only if it polls
  delivery events.
* A broadcast send needs `messages:send`; it runs the same outbound pipeline as a normal
  send. `audiences:write` covers audience membership only.
* A reputation dashboard needs `agents:read`, not `agents:write`.

Narrow keys make revocation surgical and usage simple to attribute. Prefer one key per
workload over a single shared `admin` key.

### Child keys cannot escalate

A key with `keys:write` can mint child keys, but a child's scopes must be a subset of the
key that creates it. A `messages:send`-only key cannot create a key with `domains:write`;
an `admin` key can create anything. A key can never grant more than it holds.

```bash theme={null}
# A messages:send-only key minting a domains:write key is rejected (403).
dairo api-key create --name escalate --scope domains:write
# error: Child API key scopes must be a subset of the caller scopes
```

See [API keys and authentication](/get-started/authentication) for creating, restricting,
and storing keys.
