# Third-party integration guide

> This guide covers the **ownership model** and the minimum call sequence.
> For the real-time path in depth — streaming-chat mapping, the full SSE event
> catalogue, reconnect/idempotency/retry semantics and a runnable reference
> client — see [`realtime-chat-api.md`](realtime-chat-api.md).

## Model

Your application transmits or generates conversation data. It does **not**
become the owner of that data. Every thread belongs to a human data owner, and
your application acts under an explicit delegation from that owner.

## 1. Obtain a delegated token

Your client credentials plus an `act.sub` (RFC 8693) or `on_behalf_of` claim
naming the data owner's subject. The token must carry the `application_client`
role. A delegation claim without that role is rejected.

## 2. Create a thread once per conversation

```bash
curl -X POST "$BASE/api/v1/threads" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "threadId": "your-stable-conversation-id",
    "title": "Support conversation",
    "interactionType": "general_dyad",
    "consentVersion": "your-consent-v3",
    "participants": [
      {"participantId": "user", "type": "human", "roles": ["peer"]},
      {"participantId": "bot",  "type": "assistant", "roles": ["assistant"],
       "model": "gpt-4.1-mini", "provider": "openai"}
    ]
  }'
```

Supplying your own `threadId` makes creation idempotent: a retry returns `200`
with the existing thread.

## 3. Post each contribution as it happens

```bash
curl -X POST "$BASE/api/v1/threads/$THREAD/turns" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "turnId": "msg_8412",
    "participantId": "bot",
    "speakerType": "assistant",
    "text": "…",
    "submittedAt": "2026-04-14T09:02:11.480Z",
    "firstTokenAt": "2026-04-14T09:02:12.140Z",
    "lastTokenAt":  "2026-04-14T09:02:14.980Z",
    "usage": {
      "visibleOutputTokens": 118,
      "outputTokens": 118,
      "totalApiInputTokens": 3120,
      "newInputTokens": 42,
      "cachedInputTokens": 2800
    },
    "metadata": {"streaming": true, "clientRequestId": "req_8412"}
  }'
```

The response contains the complete Layer 1 analysis. Semantic work is queued.

### What to send, and why it matters

| Field | Effect if omitted |
|---|---|
| `usage.visibleOutputTokens` | Token counts fall back to a local estimate flagged `MISSING_PROVIDER_USAGE`; ER, AR, HS and AHR become less accurate |
| `usage.totalApiInputTokens` | API Amplification Ratio is `null` |
| `firstTokenAt` / `lastTokenAt` | TTFT, generation duration, E2E and TPOT are `null` |
| `submittedAt` | The server timestamps on arrival, so latency measures reflect network delay |
| `explicitTargetParticipantIds` | Targets are inferred at lower confidence, weakening Responsivity and Social Impact |
| `parentTurnId` | Reply structure is unavailable to target resolution |

Send the provider's own numbers. Do not compute your own token counts and label
them as provider figures — provenance is enforced and inspected.

## 4. Read results

Poll `GET /threads/:threadId/analysis`, or subscribe:

```js
const events = new EventSource(`${BASE}/api/v1/threads/${threadId}/events`);
events.addEventListener('analysis.core.completed', refreshLayer1);
events.addEventListener('iccc.social_impact.revised', refreshIccc);
```

Handle `Last-Event-ID` on reconnect to replay what you missed.

## 5. Enable Layer 2 only when it applies

Layer 2 is never enabled implicitly. A two-person chat does not qualify for LCC
however long it runs: LCC requires an instructional interaction type, a
designated learner, a tutor role and a versioned reference model. If the
preconditions are unmet the API returns `not_applicable` with reasons rather
than a fabricated curve.

## Idempotency and retries

Ingestion is keyed on thread + turn. Retrying with the same `turnId` returns
`200` and the stored record. Always supply a stable `turnId` from your own
message identifiers.

## Rate limits and errors

`429` carries standard rate-limit headers; back off and retry. `409` on a
reference model means the version already exists — publish a new version. `409`
`concurrent_modification` means a manifest write raced; retry the request.

## Handling `null`

`null` always means "not measurable here", never "zero". Read the accompanying
quality flags. Do not coerce `null` to `0` in your own dashboards; it will
silently corrupt every average you compute.

A full worked client is in [`../examples/node-client.ts`](../examples/node-client.ts);
cURL recipes are in [`../examples/curl-recipes.sh`](../examples/curl-recipes.sh).
