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

# Scoped API Keys

> Mint an API key confined to a capability allowlist and specific datasets or workstreams

# Scoped API Keys

A normal API key acts as *you*: it can read every dataset, drive agents, and
mutate resources in any organization you belong to. That is the right shape for
an operator credential and the wrong shape for a server that only needs to do
one job — a portal backend that queries two datasets and writes rows into one
should not hold a credential that can do everything else too.

A **scoped key** is an `erdo_api_*` key minted with a **capability allowlist**
and, optionally, a **resource-slug allowlist**. It authenticates exactly like a
normal key, but it can only call the `/v1` endpoints its capabilities map to,
and (where slugs are given) only against the named datasets and workstreams.
Everything else returns `403 permission_denied`.

## Minting a scoped key

Mint from a session or a normal (unscoped) API key via `POST /v1/tokens` —
a scoped key can never mint keys itself:

```bash theme={null}
curl -X POST https://api.erdo.ai/v1/tokens \
  -H "Authorization: Bearer YOUR_UNSCOPED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "portal-server",
    "expires_in_days": 90,
    "capabilities": ["datasets:query", "datasets:write-rows"],
    "resource_slugs": ["acme.leads", "acme.lead-grades"]
  }'
```

The raw key is returned **once**. List and revoke scoped keys exactly like
normal keys: `GET /v1/tokens` (scoped keys carry `is_scoped`, `capabilities`,
and `resource_slugs`) and `DELETE /v1/tokens/{id}`.

A scoped key is **pinned to the organization it was minted in**. Its slug
allowlist names resources *within that org*, so the key rejects any
`X-Organization-ID` header that names a different organization — even one its
owner belongs to.

## The capability vocabulary

The vocabulary is small and **closed** — minting with any string not listed
here is rejected:

| Capability            | Grants                                                                                                                                |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `datasets:query`      | Slug-addressed dataset reads: `POST /v1/datasets/{slug}/query`, `/query-nl`, `/fetch`, and `GET /v1/datasets/{slug}/filters`          |
| `datasets:write-rows` | `POST` and `DELETE /v1/datasets/{slug}/rows`                                                                                          |
| `agents:run`          | `POST /v1/ask`, thread create/send/read, run reads                                                                                    |
| `approvals:read`      | `GET /v1/approvals`                                                                                                                   |
| `approvals:decide`    | `POST /v1/approvals/{id}/decide`                                                                                                      |
| `integrations:read`   | Integration metadata reads (never connect or delete)                                                                                  |
| `resources:read`      | Read-only resource surfaces: workstreams, experiments, pages, knowledge, KV, event pipelines, heartbeats, emails, artifacts, activity |
| `resources:write`     | The write side of those resource surfaces                                                                                             |

`GET /v1/me` works with any scoped key — it only echoes the caller's own
identity and org, and is the "does this key work" probe.

## Fail closed

Scoped keys **deny by default**:

* An endpoint that is not explicitly mapped to a capability is unreachable —
  including endpoints added to `/v1` after the key was minted, the `/mcp`
  endpoint, and the legacy SDK surface. New surface area never silently widens
  an existing scoped key.
* Deliberately unmapped today: org-wide dataset listing and search
  (`GET /v1/datasets`, `/v1/datasets-search`) and ID-addressed dataset reads —
  a slug-confined key cannot enumerate or address datasets outside its
  allowlist.
* When `resource_slugs` is non-empty, slug-addressed dataset and workstream
  calls outside the allowlist are denied even when the capability is granted.

## What no capability can grant

Regardless of the capabilities a scoped key was minted with, it can **never**:

* mint or manage credentials of any kind (`/v1/tokens`, scoped-token minting,
  the manager key),
* touch managed-organization surfaces (create, revoke, adopt, or seat members),
* connect or delete integrations, or create integration connect links.

These are hard-denied at the platform, so a leaked scoped key cannot escalate
back to the full credential it was minted to replace.
