# Authentication

The qlim8 REST API (`/api/v1`) and MCP server (`/api/mcp`) authenticate with a single kind of
credential: a **Bearer API key**.

```
Authorization: Bearer qk_live_<64-hex>      # production data
Authorization: Bearer qk_sandbox_<64-hex>   # isolated sandbox, no production data
```

- Keys are matched by a SHA-256 hash of the token; the raw value is shown **once** at creation.
- The prefix (`qk_live_` / `qk_sandbox_`) selects the environment and must match the key's real
  environment. On the dedicated hosts `api.qlim8.com` / `sandbox.api.qlim8.com` the host also
  pins the environment.
- Each key carries a set of **scopes** (least privilege). Endpoints declare the single scope
  they require; a missing scope returns `403 auth.scope_required`.
- Keys may optionally carry an IP allowlist and an expiry.

## Base URLs

| Surface | Live | Sandbox |
|---|---|---|
| REST | `https://api.qlim8.com/api/v1` | `https://sandbox.api.qlim8.com/api/v1` |
| REST (app host) | `https://app.qlim8.com/api/v1` | `https://app.qlim8.com/api/v1` |
| MCP | `https://app.qlim8.com/api/mcp` | `https://app.qlim8.com/api/mcp` |

> On `app.qlim8.com` the environment is decided by the key prefix; on the dedicated `api.` /
> `sandbox.api.` hosts it is pinned by the host (a mismatch returns
> `401 auth.key_environment_mismatch`).

## Minting a key

Mint your **first** key in the app: **Collectors → API Keys → Generate Key**. Pick the smallest
set of scopes your integration needs.

Once you hold a key with `tenant:write`, you can mint and revoke further keys programmatically
via `POST /api/v1/api-keys` (tenant keys only). The raw `key` is in the response **once**.

**curl**
```bash
curl -X POST https://api.qlim8.com/api/v1/api-keys \
  -H "Authorization: Bearer qk_live_<your-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prod-import",
    "scopes": ["emissions:read", "activities:read"]
  }'
# → 201 { "id": "...", "key": "qk_live_<new-64-hex>", "scopes": [...], ... }
```

**TypeScript (fetch)**
```ts
const res = await fetch("https://api.qlim8.com/api/v1/api-keys", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.QLIM8_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "prod-import", scopes: ["emissions:read", "activities:read"] }),
});
if (!res.ok) throw new Error(`[${res.status}] ${(await res.json()).code}`);
const { key } = await res.json();
console.log("Store this once:", key); // qk_live_<...>
```

**Python (httpx)**
```python
import os, httpx

resp = httpx.post(
    "https://api.qlim8.com/api/v1/api-keys",
    headers={"Authorization": f"Bearer {os.environ['QLIM8_KEY']}"},
    json={"name": "prod-import", "scopes": ["emissions:read", "activities:read"]},
)
resp.raise_for_status()
print("Store this once:", resp.json()["key"])
```

## Using a key & introspecting it

`GET /api/v1/me` is the canonical way to verify a key works and to see **exactly** what it can
do — its environment, key type, tenant, subscription tier, and effective scopes. A
scope-denied error never echoes your granted scopes, so use `/me` to discover them.

**curl**
```bash
curl https://api.qlim8.com/api/v1/me \
  -H "Authorization: Bearer qk_live_<your-key>"
```

**TypeScript (fetch)**
```ts
const me = await fetch("https://api.qlim8.com/api/v1/me", {
  headers: { Authorization: `Bearer ${process.env.QLIM8_KEY}` },
}).then((r) => r.json());
console.log(me.environment, me.scopes); // "live" ["emissions:read", ...]
```

**Python (httpx)**
```python
import os, httpx
me = httpx.get(
    "https://api.qlim8.com/api/v1/me",
    headers={"Authorization": f"Bearer {os.environ['QLIM8_KEY']}"},
).json()
print(me["environment"], me["scopes"])
```

Example `/me` response:
```json
{
  "api_version": "v1",
  "environment": "live",
  "key_type": "tenant",
  "key_id": "...",
  "scopes": ["emissions:read", "activities:read"],
  "tenant": { "id": "...", "name": "Acme A/S", "is_sandbox": false, "subscription_tier": "growth" },
  "third_party": null
}
```

## Scopes

Scopes are `resource:action` strings. The full set:

| Read | Write / manage |
|---|---|
| `emissions:read` | `emissions:write` |
| `activities:read` | `activities:write`, `activities:bulk` |
| `suppliers:read` | `suppliers:write` |
| `factors:read` | — |
| `reports:read` | `reports:generate`, `reports:attest` |
| `targets:read` | `targets:write` |
| `audit:read` | `audit:export` |
| `webhooks:read` | `webhooks:manage` |
| `users:read` | `users:write` |
| `tenant:read` | `tenant:write` |
| `scenarios:read` | `scenarios:write` |
| `data_sources:read` | — |
| `quota:read` | — |

## Third-party (consultant) keys

A consultant key acts on behalf of client tenants it has been granted access to. Such keys
must send the target tenant in an extra header:

```
Authorization: Bearer qk_live_<consultant-key>
X-Qlim8-Tenant: <client-tenant-id>
```

The effective scope set is the **intersection** of the key's scopes and the grant's access
scope. List accessible tenants with `GET /api/v1/grants`. Missing the header returns
`400 third_party.tenant_header_missing`; no active grant returns `403 third_party.no_active_grant`.

## Common auth errors

| HTTP | `code` | Cause |
|---|---|---|
| 401 | `auth.missing_token` | No / malformed `Authorization` header |
| 401 | `auth.invalid_token` | Key not found |
| 401 | `auth.key_revoked` | Key was revoked |
| 401 | `auth.key_expired` | Key past its `expires_at` |
| 401 | `auth.key_environment_mismatch` | Live key on a sandbox host (or vice-versa) |
| 403 | `auth.ip_not_allowed` | Source IP not in the key's allowlist |
| 403 | `auth.scope_required` | Key lacks the scope the endpoint needs |

See the [errors guide](./errors.md) for the full troubleshooting table.
