# API reference

Base path `/api/v1`. All timestamps are ISO 8601 UTC with millisecond
precision. All identifiers are opaque and must match
`[A-Za-z0-9][A-Za-z0-9_-]{0,127}`.

The machine-readable contract is [`openapi.yaml`](openapi.yaml).

Building a third-party chat integration? Start with
[`realtime-chat-api.md`](realtime-chat-api.md) — it covers the live ingestion
path, the SSE event stream, retry semantics and a reference client.

## Authentication

Production uses OIDC bearer tokens. Mock mode (development only) uses headers:

```
X-Mock-Subject: demo-owner
X-Mock-Roles: data_owner            # data_owner | researcher | application_client | system_administrator
X-Mock-On-Behalf-Of: some-owner     # requires the application_client role
```

## Health

| Method | Path | Notes |
|---|---|---|
| `GET` | `/health` | Liveness. `503` while draining. |
| `GET` | `/ready` | Validates required production configuration without disclosing secret values. |
| `GET` | `/api/v1/meta` | Algorithm versions, semantic model version, active drivers, mock-mode flag. |

## Threads and participants

| Method | Path | Notes |
|---|---|---|
| `POST` | `/threads` | Creates a thread. Idempotent on `threadId`: an existing thread returns `200`. |
| `GET` | `/threads` | Owner's threads. |
| `GET` | `/threads/:threadId` | Thread plus the caller's effective access. |
| `DELETE` | `/threads/:threadId` | `204` when deletion is enabled; otherwise `202` recording a deletion request. |
| `POST` | `/threads/:threadId/participants` | Owner only. |
| `GET` | `/threads/:threadId/participants` | Pseudonymous for researchers. |

## Contributions

| Method | Path | Notes |
|---|---|---|
| `POST` | `/threads/:threadId/turns` | `201` new, `200` idempotent replay. Returns Layer 1 immediately. |
| `POST` | `/threads/:threadId/turns/batch` | Up to 500 turns, ingested in order. |
| `GET` | `/threads/:threadId/turns` | Grant-filtered. |
| `GET` | `/threads/:threadId/turns/:turnId` | Contribution plus its core analysis. |

Ingest response:

```jsonc
{
  "created": true,
  "contribution": { /* the stored record */ },
  "analysis": {
    "layer1": { /* full per-turn measures */ },
    "layer1Status": "completed",
    "layer2Status": "queued",     // or "not_applicable"
    "speechActStatus": "queued"
  },
  "jobIds": ["job_speech_acts_thr_…_turn_…"]
}
```

## Analysis

| Method | Path | Notes |
|---|---|---|
| `GET` | `/threads/:threadId/analysis` | Thread-level Layer 1 with its versioned envelope. |
| `GET` | `/threads/:threadId/analysis/turns/:turnId` | Per-turn measures, plus speech acts under the `speech_acts` scope. |
| `POST` | `/threads/:threadId/reanalyze` | Recomputes Layer 1 and re-enqueues Layer 2. Safe to repeat. |
| `GET` | `/threads/:threadId/jobs/:jobId` | Job state. |

## Semantic configuration

| Method | Path | Notes |
|---|---|---|
| `GET` | `/threads/:threadId/semantic-extension` | Configuration and per-engine applicability with reasons. |
| `PUT` | `/threads/:threadId/semantic-extension` | Requesting an engine requires its configuration block. |
| `GET` | `/threads/:threadId/semantic-analysis` | Shared artifacts; units and embeddings are separately scoped. |

## LCC

| Method | Path |
|---|---|
| `POST` | `/lcc/reference-models` |
| `GET` | `/lcc/reference-models` |
| `GET` | `/lcc/reference-models/:referenceModelId?version=` |
| `PUT` | `/lcc/reference-models/:referenceModelId` (requires a new version) |
| `GET` | `/threads/:threadId/lcc` |
| `GET` | `/threads/:threadId/lcc/learners/:learnerId` |
| `GET` | `/threads/:threadId/lcc/turns/:turnId` |
| `POST` | `/threads/:threadId/lcc/recalculate` |

When preconditions are unmet the response is `200` with
`{"status":"not_applicable","reason":"…"}` — never fabricated curves.

## ICCC

| Method | Path |
|---|---|
| `GET` | `/threads/:threadId/iccc` |
| `GET` | `/threads/:threadId/iccc/participants` |
| `GET` | `/threads/:threadId/iccc/participants/:participantId` |
| `GET` | `/threads/:threadId/iccc/contributions/:contributionId` |
| `GET` | `/threads/:threadId/iccc/configuration` |
| `PUT` | `/threads/:threadId/iccc/configuration` |
| `POST` | `/threads/:threadId/iccc/recalculate` |
| `GET` | `/threads/:threadId/iccc/comparability?a=&b=` |

Changing the calculation mode returns a `comparabilityWarning` when existing
curves were computed differently.

## Owner retrieval and exports

| Method | Path | Notes |
|---|---|---|
| `GET` | `/me/threads` | |
| `GET` | `/me/threads/:threadId/archive` | Everything: contributions, analyses, semantic artifacts, grants, access history. |
| `POST` | `/me/threads/:threadId/exports` | Asynchronous; `202` with an export record. |
| `GET` | `/me/exports/:exportId` | Status and, once complete, a short-lived signed URL. |
| `GET` | `/me/threads/:threadId/exports/inline?format=` | Synchronous `json`, `jsonl` or `csv`. |

A signed URL is a delivery convenience issued *after* the ownership check. It is
never treated as authorization.

## Research access

| Method | Path | Notes |
|---|---|---|
| `POST` | `/threads/:threadId/grants` | Owner only. |
| `GET` | `/threads/:threadId/grants` | Owner only. |
| `DELETE` | `/threads/:threadId/grants/:grantId` | Immediate revocation. |
| `GET` | `/threads/:threadId/access-history` | Owner only. |
| `GET` | `/research/projects/:projectId/threads` | Researcher role. |
| `GET` | `/research/threads/:threadId/analysis` | Field-filtered. |
| `GET` | `/research/threads/:threadId/lcc` | Requires `lcc_curves`. |
| `GET` | `/research/threads/:threadId/iccc` | Requires `iccc_curves`. |
| `POST` | `/research/exports` | Requires `export` on each thread. |
| `GET` | `/research/grants` | The researcher's own grants. |

## Streaming

```
GET /api/v1/threads/:threadId/events
Accept: text/event-stream
Last-Event-ID: evt_…            (optional, replays from the buffer)
```

Event types: `connected`, `turn.ingested`, `analysis.core.completed`,
`analysis.speech_act.started`, `analysis.speech_act.completed`,
`analysis.speech_act.failed`, `semantic.started`, `semantic.completed`,
`lcc.updated`, `iccc.updated`, `iccc.social_impact.revised`, `analysis.failed`,
`thread.metrics.updated`, `export.completed`, `grant.updated`, `heartbeat`.

Authorization runs before the stream opens. Internal owner keys are stripped
from every frame.

## Errors

```jsonc
{ "error": { "code": "scope_required", "message": "…", "requestId": "req_…" } }
```

| Status | Codes |
|---|---|
| 400 | `validation_failed`, `unsafe_identifier`, `configuration_mismatch` |
| 401 | `unauthenticated`, `invalid_token`, `task_unauthenticated` |
| 403 | `forbidden`, `scope_required`, `owner_required`, `role_required` |
| 404 | `not_found` |
| 409 | `version_conflict`, `concurrent_modification`, `incompatible_semantic_model`, `thread_deleting` |
| 429 | `rate_limited` |
| 500 | `internal_error`, `task_failed` |
