CormorantDocs
Menu

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
curl
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_...
curl
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"])
TypeScript
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.

View as Markdown