diff --git a/apps/docs/src/content/docs/reference/api-keys.mdx b/apps/docs/src/content/docs/reference/api-keys.mdx index a71c308d9..bbd35d3de 100644 --- a/apps/docs/src/content/docs/reference/api-keys.mdx +++ b/apps/docs/src/content/docs/reference/api-keys.mdx @@ -87,11 +87,13 @@ Scopes can be updated after creation via the `PATCH` endpoint without rotating t ### Authentication Flow -To authenticate with an API key, include it in the `X-API-Key` header: +To authenticate with an API key, include it in the `X-API-Key` header — on one of +the routes that accept it (see [Where API keys work](#where-api-keys-work) below; +the general REST API is not one of them): ```bash curl -H "X-API-Key: brz_aBcDeFgHiJkLmNoPqRsTuVwXyZ012" \ - https://breeze.yourdomain.com/api/v1/devices + https://breeze.yourdomain.com/api/v1/devices/$DEVICE_ID/custom-fields ``` The middleware performs these steps in order: @@ -116,9 +118,28 @@ The middleware performs these steps in order: | `X-RateLimit-Reset` | Unix timestamp when the window resets. | | `Retry-After` | Seconds until the rate limit resets (only on 429 responses). | +### Where API keys work + +An API key authenticates **three surfaces**, not the API as a whole: + +| Endpoint | Required scope | +|---|---| +| `/api/v1/mcp/*` — the [MCP server](/features/mcp-server/) | `ai:*` | +| `POST /api/v1/dev/push` | `devices:execute` | +| `GET /api/v1/devices/:id/custom-fields` | `devices:read` | +| `PATCH /api/v1/devices/:id/custom-fields` | `devices:write` | + +Every other route authenticates from the `Authorization` header alone and never +looks at `X-API-Key`, so a key sent anywhere else returns +`401 Missing or invalid authorization header`. A key with `devices:read` will not +authenticate `GET /api/v1/devices` — that route needs a user JWT. Scopes gate what +a key may do on the routes above; they do not widen where it is accepted. + ### Dual Authentication -Some endpoints accept either a JWT bearer token or an API key. When both headers are present, the API key takes precedence: the route handler checks for `X-API-Key` first and authenticates via `apiKeyAuthMiddleware`, falling back to `authMiddleware` only when that header is absent. +The routes listed above accept either a JWT bearer token or an API key. When both headers are present, the API key takes precedence: the route handler checks for `X-API-Key` first and authenticates via `apiKeyAuthMiddleware`, falling back to `authMiddleware` only when that header is absent. + +On the JWT path some of them are stricter than the key path: `PATCH /devices/:id/custom-fields` and `POST /dev/push` additionally require MFA, matching the permissions they mirror. ### Key Rotation diff --git a/apps/docs/src/content/docs/reference/api.mdx b/apps/docs/src/content/docs/reference/api.mdx index 4919b4c0c..c35e42e21 100644 --- a/apps/docs/src/content/docs/reference/api.mdx +++ b/apps/docs/src/content/docs/reference/api.mdx @@ -23,13 +23,42 @@ curl -H "Authorization: Bearer $TOKEN" \ https://breeze.yourdomain.com/api/v1/devices ``` -API keys can also be used via the `X-API-Key` header: +### API keys are not general-purpose auth + + + +The surfaces that accept `X-API-Key`: + +| Endpoint | Required key scope | +|---|---| +| `/api/v1/mcp/*` — the [MCP server](/features/mcp-server/) | `ai:*` | +| `POST /api/v1/dev/push` | `devices:execute` | +| `GET /api/v1/devices/:id/custom-fields` | `devices:read` | +| `PATCH /api/v1/devices/:id/custom-fields` | `devices:write` | + +Those four routes are dual-auth: they use the key when `X-API-Key` is present +and fall back to a user JWT when it is absent. Everything else is JWT-only. ```bash +# Works — a dual-auth route. +curl -H "X-API-Key: brz_..." \ + https://breeze.yourdomain.com/api/v1/devices/$DEVICE_ID/custom-fields + +# Returns 401 — the devices list is JWT-only. curl -H "X-API-Key: brz_..." \ https://breeze.yourdomain.com/api/v1/devices ``` +If you are scripting against Breeze, log in and use the resulting JWT. Note that +some write routes additionally require MFA on the JWT path — including +`POST /orgs/partners`, `POST /orgs/organizations` and `POST /orgs/sites` — so an +account without MFA enrolled cannot create tenancy unattended. + ### Login ```bash