# Authentication & keys

## Project keys

Cormorant authenticates **projects**, not people. Each API key (`cmr_…` or `cma_…`)
belongs to exactly one project; the project owns its usage, its monthly budget
for paid providers, its rate limit and its options (such as allowed
User-Agents). Keys are issued and revoked by the operator; ask for one key per
application or environment so you can rotate them independently.

Cormorant stores only an HMAC of each key. A lost key cannot be recovered — ask
for a new one and have the old one revoked.

## Account admin key and additional keys

Every key has a **role**, fixed when it is issued and visible in its prefix:

| Key | Prefix | What it can see |
|---|---|---|
| **Account admin key** | `cma_…` | the whole customer account, where an endpoint offers it: all keys, projects, usage and invoices |
| **Additional key** | `cmr_…` | its own usage only |

Keep the account admin key with whoever manages the account (billing, key
overview) and give applications additional keys. To change a key's role, ask
for a new key with the other role and have the old one revoked. Keys issued
before roles existed are additional (`cmr_…`) keys.

Every authenticated response tells you which kind of key you used:

```text
X-Cormorant-Key-Role: account_admin
```

```sh
curl -sSI https://api.cormorant.ch/v1/usage \
  -H "Authorization: Bearer $CORMORANT_API_KEY" | grep -i x-cormorant-key-role
```

## Sending the key

Every `/v1` request needs the key as a Bearer token:

```text
Authorization: Bearer cmr_...
```

```sh
curl -sS https://api.cormorant.ch/v1/usage \
  -H "Authorization: Bearer $CORMORANT_API_KEY"
```

```python
import os

import httpx

client = httpx.Client(
    base_url="https://api.cormorant.ch",
    headers={"Authorization": f"Bearer {os.environ['CORMORANT_API_KEY']}"},
    timeout=180,
)
print(client.get("/v1/usage").json()["project"])
```

```ts
const res = await fetch("https://api.cormorant.ch/v1/usage", {
  headers: { Authorization: `Bearer ${process.env.CORMORANT_API_KEY}` },
});
console.log((await res.json()).project);
```

`/health` and `/ready` need no key. Everything under `/v1` does, including
`GET /v1/bodies/{sha256}` — a `body_url` is not a public link.

## When authentication fails

| Status | Body | Meaning |
|---|---|---|
| `401` | `{"detail": "missing bearer token"}` | No `Authorization: Bearer …` header. |
| `401` | `{"detail": "invalid API key"}` | Unknown or revoked key, or a disabled project. |
| `403` | `{"detail": {"error": "customer_suspended", "message": "…"}}` | The customer account that owns the project is suspended. |
| `403` | `{"detail": {"error": "user_agent_not_allowed", "hint": "…"}}` | The request used a `user_agent` that is not on the project's allowlist. |

`401` responses carry `WWW-Authenticate: Bearer`. Do not retry them: fix the
key.

## Rate limits

Each project has a request-rate limit (by default 200 requests per second, in
a fixed one-second window, shared by all of the project's keys). Excess
requests get `429 {"detail": "rate limit exceeded"}` with `Retry-After: 1`.
A batch call counts as **one** request however many items it carries — another
reason to use batches for bulk work. See [Limits & politeness](/docs/limits.md).

## Request ids

Every response carries a `request_id` (`req_…`; batch items use
`<job_id>:<index>`). Log it. When something looks wrong, give the operator the
request id: the full attempt history of every request is kept.

## Keeping keys safe

- Read the key from the environment or a secret manager; never commit it,
  never embed it in browser or mobile code, never put it in URLs.
- Use separate keys (projects) for production, staging and experiments so
  budgets and usage stay separate.
- For AI agents, give the agent a dedicated project with a modest budget, and
  pass the key to the [MCP server](/docs/mcp.md) through its
  environment — not through the prompt.
