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:
X-Cormorant-Key-Role: account_admin
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:
Authorization: Bearer cmr_...
curl -sS https://api.cormorant.ch/v1/usage \
-H "Authorization: Bearer $CORMORANT_API_KEY"
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"])
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.
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 through its environment — not through the prompt.