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

# Scopes

> Seven scopes: three umbrellas and four fine-grained grants. What each one unlocks, and the two implications worth knowing before you mint.

**A key holds a set of scopes, and a route refuses any key that does not hold the one it needs.** There are seven names in the vocabulary: the umbrella scopes `read`, `write` and `admin`, and the fine-grained scopes `graph:write`, `memory:forget`, `subjects:provision` and `usage:read`.

`GET /v1/me` returns the scopes a key actually holds. Start there when a call returns `403`.

## The vocabulary

| Scope                | Also satisfies                     | What it unlocks                                                                                                                                                                          |
| -------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `read`               | nothing else                       | Retrieval, search, recall, conditioning, brain reads, graph reads, job and import status, contradictions, proposals, settings, webhook reads, export reads, your own organization record |
| `write`              | `graph:write`, `memory:forget`     | Ingest, import, session ingestion, job cancellation, connector management, webhook management, exports, and the full graph mutation and erasure surface                                  |
| `admin`              | `subjects:provision`, `usage:read` | Key management, sandbox management, org-wide usage and exports, team member listing, brain training, purge, and account deletion                                                         |
| `graph:write`        | nothing else                       | Edit a node, assert an edge, supersede a belief, restore a tombstoned node, read a node's mutation history                                                                               |
| `memory:forget`      | `graph:write`                      | Forget a node, remove an edge. The erasure grant                                                                                                                                         |
| `subjects:provision` | nothing else                       | Provision and erase subjects                                                                                                                                                             |
| `usage:read`         | nothing else                       | The org-wide metering view at `GET /v1/usage`                                                                                                                                            |

Satisfaction is transitive down that middle column. A requirement is met by the scope itself or by any scope that satisfies it, so a `write` key passes a `memory:forget` check, and a `memory:forget` key passes a `graph:write` one.

<Warning>
  **The three umbrella scopes are not cumulative.** `write` does not satisfy a `read` requirement, and `admin` satisfies neither `read` nor `write`. They are three separate grants that happen to be named like a ladder.

  In practice this means you list every umbrella you need. A key minted with `["write"]` alone can call `POST /v1/ingest` and then fail with 403 on `GET /v1/ingest/{job_id}/status`, because polling is a `read` route. Mint `["read", "write"]` for anything that writes and then checks its work. Sandbox keys are minted `["read", "write"]` for exactly this reason.
</Warning>

## Three implications worth knowing

<AccordionGroup>
  <Accordion title="graph:write does not grant memory:forget" icon="shield-check">
    This is the point of splitting them. A key with `graph:write` can edit the knowledge graph, assert relations, supersede beliefs and restore a node someone tombstoned. It cannot forget anything: `DELETE /v1/graph/nodes/{node_id}` and `DELETE /v1/graph/edges/{edge_id}` both require `memory:forget`, and `graph:write` alone cannot remove structure.

    That lets you mint a curation key for an agent or an internal tool that is allowed to improve the graph and is structurally incapable of erasing it. A `write` key holds both, so if you want the separation you have to ask for it explicitly.
  </Accordion>

  <Accordion title="memory:forget does grant graph:write, in that direction only" icon="arrow-right">
    The implication runs one way. Anything able to erase a node is also able to edit one, so `memory:forget` satisfies a `graph:write` requirement. The reverse is what the previous section rules out. If you want an erase-only credential, there is no such thing: erasure is the strictly wider grant.
  </Accordion>

  <Accordion title="A read key cannot read org-wide usage" icon="chart-no-axes-column">
    `GET /v1/usage` aggregates every subject's usage into one billing view, so it is not a `read` surface. It requires `usage:read`, which an `admin` key also qualifies for. A plain `read` key is refused.

    `POST /v1/exports` follows the same posture: an export at `scope: "user"` needs `write`, while `scope: "org"` exports every member's data and additionally requires `admin`. `GET /v1/team` is a `read` route, because it returns the same fields `GET /v1/me` already gives you, but enumerating the members with `GET /v1/team/members` requires `admin`.
  </Accordion>
</AccordionGroup>

## What each route needs

The routes below need more than `read`. Everything else in `/v1` is a `read` surface, and `GET /v1/me` needs only a valid key.

### Write

| Route                                                                                                          | Scope                                                        |
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `POST /v1/ingest`, `POST /v1/ingest/batch`                                                                     | `write`                                                      |
| `POST /v1/import`, `POST /v1/import/transcript`                                                                | `write`                                                      |
| `POST /v1/ingest/session-event`, `/session-batch`, `/session-finalize`, `/agent-signals`                       | `write`                                                      |
| `GET /v1/ingest/session-status`                                                                                | `write`, not `read`. It sits on the session-ingestion family |
| `POST /v1/jobs/{job_id}/cancel`                                                                                | `write`                                                      |
| `DELETE /v1/recall/sessions/{session_id}`                                                                      | `write`                                                      |
| `POST /v1/proposals/{proposal_id}/accept`, `POST /v1/proposals/{proposal_id}/reject`                           | `write`                                                      |
| `PATCH /v1/settings`, `POST /v1/settings/reset`                                                                | `write`                                                      |
| `POST /v1/connectors/{connector}`, `POST /v1/connectors/{connector}/sync`, `DELETE /v1/connectors/{connector}` | `write`                                                      |
| `POST /v1/webhooks`, `PATCH /v1/webhooks/{id}`, `DELETE /v1/webhooks/{id}`, `POST /v1/webhooks/{id}/test`      | `write`                                                      |
| `POST /v1/exports`                                                                                             | `write`, plus `admin` when `scope` is `"org"`                |

### Graph mutation and erasure

| Route                                      | Scope           | A `write` key also qualifies |
| ------------------------------------------ | --------------- | ---------------------------- |
| `PATCH /v1/graph/nodes/{node_id}`          | `graph:write`   | Yes                          |
| `POST /v1/graph/edges`                     | `graph:write`   | Yes                          |
| `POST /v1/graph/nodes/{node_id}/supersede` | `graph:write`   | Yes                          |
| `POST /v1/graph/nodes/{node_id}/undelete`  | `graph:write`   | Yes                          |
| `GET /v1/graph/nodes/{node_id}/history`    | `graph:write`   | Yes                          |
| `DELETE /v1/graph/nodes/{node_id}`         | `memory:forget` | Yes                          |
| `DELETE /v1/graph/edges/{edge_id}`         | `memory:forget` | Yes                          |

Node history is read-shaped but sits on the write-managed family, because it reads the mutation record rather than the content.

### Admin

| Route                                                                                                     | Scope                | An `admin` key also qualifies |
| --------------------------------------------------------------------------------------------------------- | -------------------- | ----------------------------- |
| `POST /v1/keys`, `GET /v1/keys`, `DELETE /v1/keys/{key_id}`                                               | `admin`              |                               |
| `PUT /v1/subjects/{subject_id}`, `DELETE /v1/subjects/{subject_id}`                                       | `subjects:provision` | Yes                           |
| `GET /v1/usage`                                                                                           | `usage:read`         | Yes                           |
| `GET /v1/team/members`                                                                                    | `admin`              |                               |
| `POST /v1/brain/train`                                                                                    | `admin`              |                               |
| `POST /v1/sandbox/keys`, `GET /v1/sandbox/keys`, `DELETE /v1/sandbox/keys/{id}`, `POST /v1/sandbox/reset` | `admin`              |                               |
| `POST /v1/purge`                                                                                          | `admin`              |                               |
| `DELETE /v1/me`                                                                                           | `admin`              |                               |

`GET /v1/subjects` and `GET /v1/subjects/{subject_id}` are `read` routes. Only provisioning and erasure need `subjects:provision`.

<Note>
  Signed-in dashboard sessions may mint keys without holding `admin`. The requirement applies to API keys calling `POST /v1/keys`, which is what stops a `write` key from minting itself a way around its own scopes.
</Note>

## Minting a narrower key

`POST /v1/keys` takes an optional `scopes` array. Omit it and the new key inherits the caller's scopes. Name a subset and the new key is narrower. Name a scope the caller does not hold and the mint is a `403`, so minting can never escalate.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl -X POST https://api.get-exo.com/v1/keys \
    -H "X-Exo-API-Key: $EXO_ADMIN_KEY" \
    -H "Content-Type: application/json" \
    -d '{"label": "curation agent", "scopes": ["read", "graph:write"]}'
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  requests.post(
      "https://api.get-exo.com/v1/keys",
      headers={
          "X-Exo-API-Key": os.environ["EXO_ADMIN_KEY"],
          "Content-Type": "application/json",
      },
      json={"label": "curation agent", "scopes": ["read", "graph:write"]},
  )
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  await fetch("https://api.get-exo.com/v1/keys", {
    method: "POST",
    headers: {
      "X-Exo-API-Key": process.env.EXO_ADMIN_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      label: "curation agent",
      scopes: ["read", "graph:write"],
    }),
  });
  ```
</CodeGroup>

That key can read, edit the graph and supersede beliefs. It cannot forget a node, provision a subject, mint another key or see org-wide usage. It is the shape most agent integrations should use.

## Sandbox keys ignore all of this

A sandbox key is minted with `read` and `write`, fixed, not caller-selectable. There is no way to widen or narrow it, because a test credential must never be able to mint keys, provision subjects or erase real data. See [Sandbox](/platform/sandbox).

## What a refusal looks like

A key that authenticates but lacks the scope gets `403` with `permission_denied`, not `401`. The same status covers a subject selector that was never provisioned, so read the `detail` before assuming which one you hit.

```json 403 application/problem+json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "type": "https://docs.get-exo.com/errors/permission_denied",
  "title": "Permission denied",
  "status": 403,
  "detail": "This key does not carry the memory:forget scope.",
  "code": "permission_denied",
  "requestId": "req_3f6c1d7a90b24e5a",
  "documentationUrl": "https://docs.get-exo.com/errors/permission_denied",
  "suggestedAction": "Mint a key with the required scope, or call this route with an admin key."
}
```

The `code` is always the tail of the `type` URI, so `documentationUrl` resolves to a real page on this site. See [Errors](/platform/errors) for the full envelope.

## Next

<Columns cols={2}>
  <Card title="Authentication" icon="key" href="/authentication">
    Headers, minting, revocation, and what a leaked key can and cannot reach.
  </Card>

  <Card title="One memory per end user" icon="users" href="/guides/subjects">
    Where `subjects:provision` is the scope you actually need.
  </Card>
</Columns>
