# REST API

Base: `https://api.doubleagent.so`. Errors look like `{ "error": { "code", "message" } }`.

| Auth | Header |
|---|---|
| none | – |
| pk | `?k=pk_…` |
| session | `Authorization: Bearer das_…` (portal, CLI login) |
| sk | `Authorization: Bearer sk_…` |

## Collection and tokens

| Route | Auth | Returns |
|---|---|---|
| `POST /v1/collect` | none (keyless, by Origin) or pk | 204 (the SDK beacon) |
| `POST /v1/check` | pk | `{ verdict, token }`; keyless → 401 `key_required` |
| `POST /v1/ping` | pk | Live presence |
| `POST /v1/verify` | none | `{ valid, claims }` |
| `GET /.well-known/jwks.json` | none | Signing keys |
| `GET /v1/install-check?url=` | none | Install report (see [/install.md](/install.md) step 5) |

## Data (403 `site_not_verified` until a domain is verified)

| Route | Auth | Returns |
|---|---|---|
| `GET /v1/stats?from&to` | sk, or session + `?site=st_…` | Totals, series, families, profiles |
| `GET /v1/sessions?limit&class&cursor&quarantined` | sk, or session + `?site=` | `{ sessions, nextCursor }` |
| `GET /v1/sessions/:sid` | sk, or session + `?site=` | One [Dossier](/docs/hq) |
| `GET /v1/live?window=60` | sk, or session + `?site=` | Live now |
| `GET /v1/sessions/:sid/events?limit&cursor` | sk, or session + `?site=` | `{ events, nextCursor }`: every recorded revision of one session (collect, check, judge, join), `limit` ≤ 200 |
| `GET /v1/events/export?limit&cursor` | sk, or session + `?site=` | `{ events, nextCursor }`: all events for the site, oldest first, `limit` ≤ 200 (default 100) |

Every session is stored, humans included, and appears in these routes within about 30 seconds. `/v1/stats` accepts ranges up to 365 days. After a domain claim merges keyless history, data routes answer `503 claim_pending` (retry shortly) until it is consolidated.

Collection telemetry: `POST /v1/collect` answers with header `DA-Telemetry-Accepted: 0` when the event could not be queued for storage (detection still succeeded). `POST /v1/check` always includes `telemetryAccepted: true|false` in its JSON. Both only say whether *storing* the session was queued; they never affect the verdict.

## Accounts and auth

| Route | Auth | Purpose |
|---|---|---|
| `POST /v1/auth/login` | none | `{email}` → 204 (never says whether the email exists) |
| `POST /v1/auth/verify` | none | `{token}` → `{ session, user, accounts }` |
| `POST /v1/auth/logout` | session | `{all?}` → 204 |
| `GET /v1/me` | session | User, accounts, sites |
| `POST /v1/auth/device`, `/device/token`, `/device/approve` | none / session | CLI device flow |
| `POST /v1/accounts` | none + `DA-PoW` | Create account + site + keys ([Accounts](/docs/accounts)) |

## Sites, domains, keys (session; viewers read only)

| Route | Purpose |
|---|---|
| `GET/POST /v1/sites` | List; create `{name, domain?, profile?}` |
| `GET/PATCH/DELETE /v1/sites/:id` | Settings; webhook and integration secrets are write-only |
| `POST /v1/sites/:id/domains` | `{hostname}` → token and methods |
| `POST /v1/sites/:id/domains/:host/verify` | `{method}` → `{ verified, detail, claimed }` |
| `DELETE /v1/sites/:id/domains/:host` | Remove a domain |
| `GET/POST /v1/sites/:id/keys` | List (prefixes only); create `{kind, env}`, sk shown once |
| `POST /v1/sites/:id/keys/:keyId/rotate` | New key; the old one lasts 24 h |
| `DELETE /v1/sites/:id/keys/:keyId` | Revoke now |
| `DELETE /v1/sites/:id/preclaim` | Discard pre-verification data |
| `GET /v1/sites/:id/audit` | Audit log |

Limits: [Security](/docs/security).
