Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions apps/docs/src/content/docs/reference/api-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
31 changes: 30 additions & 1 deletion apps/docs/src/content/docs/reference/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

<Aside type="caution" title="An API key will not authenticate the general REST API">
`X-API-Key` is accepted on **three** surfaces only. Every other route reads the
`Authorization` header and nothing else, so sending a key to (for example)
`GET /api/v1/devices` returns `401 Missing or invalid authorization header` —
the key is never consulted.
</Aside>

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
Expand Down