# Scopes

Scopes are the unit of authorization on Roam API tokens (API keys, OAuth grants,
and Personal Access Tokens). Request them at OAuth consent (space-separated
`scope` query parameter) or when creating a PAT / API key in Developer settings.

Canonical names and descriptions are defined in the server scope registry
(`common/apiScopes.json`). Endpoint pages restate the **Required scope** for
each operation; if a page and this catalog disagree, trust the running API and
file a docs bug.

## Requesting scopes

**OAuth authorize URL** — space-separated list:

```text
scope=chat:history%20chat:send_message%20user:read
```

**PAT creation UI** — choose PAT **groups** (below). Groups expand to concrete
scopes stored on the token.

**Aliases:** the only documented alias today is `groups:read` → canonical
`group:read`. Comparisons always use the canonical form. Some write endpoints
also accept the legacy alternate **`chat:write`** as a synonym for
`chat:send_message` at the require-scope check (it is not a separately
grantable scope in the registry).

## OAuth / API key scopes

| Scope | Description |
|-------|-------------|
| `addr:info` | Read information about a chat participant (historical spelling; public resolution is `user.info` / `group.info`) |
| `admin:compliance:read` | Read certain compliance data exports |
| `admin:meetings:read` | Org only. View all meetings, recordings, and transcripts in this Roam (see [Meeting width](#meeting-width-adminmeetingsread)) |
| `calendar:read` | Read events from a user's connected calendars (`calendar.list`) |
| `calendar:write` | Create and update calendar events |
| `chat:history` | Read messages from public chats, or chats the app has been added to |
| `chat:join` | Join public chats |
| `chat:read` | List public chats, or ones the bot has been added to |
| `chat:send_message` | Send chat messages (also accepted as `chat:write` on write endpoints) |
| `commands` | Slash commands and message actions (interactivity; limited public surface today) |
| `group:read` | View public groups (`groups:read` is a legacy alias) |
| `group:write` | Create and manage groups; `group.join` (self-join a public group) |
| `item:read` | View item / attachment details |
| `item:write` | Create items / uploads |
| `links:read` | Receive links shared in chats the app can see (`chat.link.shared`) |
| `links:write` | Attach rich previews via `chat.unfurl` |
| `lobby:read` | View lobbies and lobby bookings |
| `magicast:read` | View Magicasts |
| `meetinglink:read` | View meeting links |
| `meetinglink:write` | Create / update meeting links |
| `meetings:read` | View meetings the app has access to (see [Meeting width](#meeting-width-adminmeetingsread); [`calendar:read`](#calendarread-and-meetingsread) for calendars) |
| `onair:read` | View On-Air events, guests, and hosts |
| `onair:write` | Create and manage On-Air events, guests, and hosts |
| `recordings:read` | View meeting recordings the app has access to (legacy `recording.list` surface) |
| `transcript:read` | View meeting transcripts the app has access to (prefer `meetings:read` on v1) |
| `user:read` | List / resolve users |
| `user:read.email` | Include user email addresses |
| `user:read.status` | View member check-in status and will-return times; required for `user.status.update` webhooks |
| `user:write` | Create and update users |
| `userauditlog:read` | Read user audit logs |
| `webhook:read` | List webhook subscriptions and failed deliveries |
| `webhook:write` | Subscribe / unsubscribe webhooks; lifecycle events |

Hidden / legacy registry entries (`create_tags`, `meeting:write`, hidden
`groups:read`) may appear in older grants; prefer the canonical names above.

### `calendar:read` and `meetings:read`

Reading a user's connected calendars ([`calendar.list`](/docs/api/calendar-list),
and the `calendar_list` MCP tool) requires **`calendar:read`**. That access used
to fall under `meetings:read`.

| Client registered | Scope that grants calendar access |
|---|---|
| Before **2026-07-29T00:00Z** | `meetings:read` (grandfathered) or `calendar:read` |
| On or after **2026-07-29T00:00Z** | `calendar:read` only |

A client registered on or after the cutoff that holds only `meetings:read` gets
`403` / `missing_scope` naming `calendar:read` when it calls `calendar.list`.
Existing grants keep working; new authorizations should request `calendar:read`.
Requesting both is harmless if one codebase serves grants from either side of
the cutoff.

This affects calendar access only — `meetings:read` is unchanged for meetings,
transcripts, participants, search, and prompt.

Personal Access Tokens created with the
[`pat:meetings:read`](#personal-access-token-groups) group are unaffected on
either side of the cutoff: the group expands to include `calendar:read`.

### Meeting width (`admin:meetings:read`)

`meetings:read` / `recordings:read` / `transcript:read` are the capability to
call those APIs and subscribe to those events. They do **not** mean every
meeting in the Roam. Width is:

| Credential | Meetings, recordings, transcripts, and those webhooks |
|---|---|
| **Personal** | Meetings the token owner participated in. Personal tokens never get roam-wide access — `admin:meetings:read` is stripped on authorize and is not in any PAT group. |
| **Org without `admin:meetings:read`** | Meetings the install's bot already has access to. This set is usually empty unless the bot was a participant on the saved meeting. |
| **Org with `admin:meetings:read`** | Every meeting, recording, and transcript in the installed Roam. |

API keys default `admin:meetings:read` **on** in Developer Settings when you
enable a meeting scope (the Sales Ops / CRM path: an admin-issued key should
see every recording). OAuth apps default it **off** (the app developer is not
the installing account). Adding the scope to an OAuth app does not widen
existing tokens until they re-authorize.

Existing API keys that already had roam-wide meeting access keep it without
the new scope. There is no Meeting Access toggle — that control is gone.

`/v0/meeting.list`, `/v1/conversation.list`, and `/v1/recording.list` cannot
filter by bot access today. Org clients without roam-wide meeting access get
`403`; use [`/meeting.list`](/docs/api/meeting-list) instead.

This is the same split chat already has: ordinary scopes for “what you can
see,” `admin:compliance:read` for company-wide export. All-recordings is the
original public API, not a compliance product.

### Vocab notes (honest)

- **`addr:info`** is a historical spelling (not `address:info`). Public
  principal and group resolution is `user.info` / `group.info`.
- **`meetings:read`** / **`recordings:read`** use plural nouns; most other
  scopes are singular (`group:read`, `user:read`).
- **`chat:write`** is accepted by many write handlers alongside
  `chat:send_message` but is not a first-class grantable name in the scope
  picker registry.

## Webhook event → required scope

Subscribe requires the listed scope (and delivery enforces it). Enrichment
scopes (e.g. `user:read.email`) are additional when noted on the event page.

| Event | Required scope |
|-------|----------------|
| `chat.message` | `chat:history` |
| `chat.reaction` | `chat:history` |
| `chat.link.shared` | `links:read` |
| `meeting.started` | `meetings:read` (width: [Meeting width](#meeting-width-adminmeetingsread)) |
| `meeting.ended` | `meetings:read` (width: [Meeting width](#meeting-width-adminmeetingsread)) |
| `user.status.update` | **`user:read.status`** (not `user:read` alone) |
| `lobby.booked` | `lobby:read` |
| `magicast.created` | `magicast:read` |
| `onair.event.created` | `onair:read` |
| `onair.event.updated` | `onair:read` |
| `onair.event.canceled` | `onair:read` |
| `onair.guest.added` | `onair:read` |
| `onair.guest.rsvp` | `onair:read` |
| `token.revoked` | `webhook:write` |
| `app.uninstalled` | `webhook:write` |

## Personal Access Token groups

PAT creation offers grouped scopes that expand server-side:

| PAT group | Expands to (canonical) |
|-----------|-------------------------|
| `pat:chat:read` | `chat:read`, `chat:history`, `item:read`, `addr:info`, `user:read`, `user:read.email`, `group:read`, `webhook:read`, `webhook:write` |
| `pat:chat:write` | `chat:send_message`, `item:write`, `user:read`, `user:read.email`, `group:read`, `webhook:read`, `webhook:write` |
| `pat:meetings:read` | `meetings:read`, `calendar:read`, `recordings:read`, `transcript:read`, `meetinglink:read`, `lobby:read`, `user:read`, `user:read.email`, `webhook:read`, `webhook:write` |
| `pat:magicast:read` | `magicast:read`, `user:read`, `user:read.email`, `webhook:read`, `webhook:write` |
| `pat:calendar:write` | `calendar:write`, `user:read`, `user:read.email`, `webhook:read`, `webhook:write` |
| `pat:groups:write` | `group:write`, `group:read`, `user:read`, `user:read.email`, `webhook:read`, `webhook:write` |

`token.info` reports the **expanded** OAuth scopes on the credential, not the
`pat:*` group names.

`pat:meetings:read` does **not** include `admin:meetings:read`. Personal tokens
cannot hold roam-wide meeting access.

## Missing scope errors

When a token lacks a required scope, the API returns HTTP `403` with
`error: "missing_scope"` and machine-readable `needed` / `provided` arrays.
See [Responses and Errors](/docs/guides/responses-and-errors#missing_scope-details).

## Related guides

- [OAuth & Authentication](/docs/guides/oauth)
- [Access Models](/docs/guides/access-models)
- [Migrating from v0 to v1](/docs/guides/migration-v0-to-v1) (scope re-consent)