# Authorization

What each credential may do, over REST and over the remote MCP endpoint. A credential of the
wrong kind for a REST route is `403 forbidden`; a resource that exists but belongs to someone
the credential can't see is `404 not_found`, never `403`.

## REST

| Operation | Account key | Device credential | Send token | Poll secret | None |
|---|---|---|---|---|---|
| Send (`POST /v1/notifications`, `/v1/send/{key}`) | no (`403 forbidden`) | no | **yes** | no | no |
| `GET /v1/scenes` | yes | yes | yes | no | no |
| Status (`GET /v1/notifications/{id}/status`) | any message of the account | messages to its account | **only messages this token created** | no | no |
| `GET /v1/devices` | full | its own device | **names and platforms only** | no | no |
| `GET /v1/inbox`, `GET /v1/notifications/{id}` | yes | yes | no | no | no |
| `POST /v1/notifications/{id}/handled` | no (needs a device) | yes | no | no | no |
| `GET /v1/usage` | yes | yes | no | no | no |
| `PUT` / `DELETE /v1/devices/{id}` | any device | only the device id its credential is bound to | no | no | no |
| Tokens (`/v1/tokens…`) | yes | no | no | no | no |
| Account (`/v1/accounts/me…`, rotate, merge, delete) | yes | no | no | no | no |
| Link: inspect, approve, reject by code (`/v1/link/requests/{code}/details`, `/approve`, `/reject`) | yes | no | no (`403`) | no | no |
| Link: poll, cancel by grant (`/v1/link/requests/{grant_id}`) | no | no | no | yes | no |
| Link: switch accounts (`POST /v1/link/requests/{grant_id}/switch`, #82) | no (the source, in the body) | **only the credential the grant reserved**, before it registers | no (`403`) | no (`403`) | no |
| `POST /v1/link/requests`, `POST /v1/accounts`, `GET /v1/health` | — | — | — | — | yes (rate limited) |

A send token's read access is two scopes, both on by default: `own_status` (the status of
messages it sent) and `device_names` (names and platforms, for choosing a `target`). Without
`own_status`, every status lookup is `404`; without `device_names`, `GET /v1/devices` is
`403 forbidden`.

Poll-secret routes answer anything but the request's own poll secret with `401`, so they
never reveal whether a grant id exists.

## MCP (`POST /mcp`)

The endpoint takes the credentials `GET /v1/scenes` takes: a send token, an account key or a
device credential. A link poll secret is `403 forbidden` at the endpoint, and a missing,
unknown, revoked or expired credential is `401`. Over MCP, everything but `list_scenes` needs
a send token:

| Tool | Account key | Device credential | Send token |
|---|---|---|---|
| `list_scenes` | yes | yes | yes |
| `send_notification` | no | no | **yes** |
| `get_notification_status` | no | no | **only messages this token created** (needs `own_status`) |
| `list_devices` | no | no | **names and platforms only** (needs `device_names`) |

An account key or device credential sees only `list_scenes` in `tools/list`. Calling another
tool with one is a tool result with `isError: true` and the error code `forbidden`, not an
HTTP error. With a send token, each tool runs its REST route's code, so the same scopes and
`404`s apply.
