# Conversation integration examples — unpublished candidate

These clients cover the full Conversation contract. They make **one explicitly
selected request per invocation**, with no automatic retries, voice purchases or
continuations. They do not enable an account or authorize a paid test. Public
access is closed and the current release candidate has voice disabled.

Use Node 24+ for `conversation.ts`, Python 3.11+ for `conversation.py`, or Bash/Git
Bash with cURL and jq for `conversation.sh`. The examples have offline proofs;
they have not been run against a hosted Conversation assignment.

Store the API key in the server environment as `FOREVERLEARNING_API_KEY`. Never
put it in a request file, browser, command-line argument, source repository or log.
Use a private local directory for request/response files: they can contain learner
content. On Windows, use your user-owned directory and its access controls.

## Save the intent before dispatch

Every request file fixes the assigned host, assignment and pseudonymous user, so
a retry cannot silently move to another environment or user. Replace these
placeholders with your approved assignment. Generate a UUID **once per new
mutation**, persist it, and retain that same ID and body on retries. A new user
action gets a new file and UUID. For lifecycle bodies, use that UUID as the
`operation_id` too.

```json
{
  "base_url": "https://ASSIGNED-API-HOST/v1",
  "assignment_id": "ASSIGNED-UUID",
  "external_user_id": "synthetic-practice-user",
  "method": "GET",
  "path": "/conversation/catalog"
}
```

```sh
node conversation.ts catalog.request.json catalog.response.json
python conversation.py catalog.request.json catalog.response.json
bash conversation.sh catalog.request.json catalog-response-directory
```

Choose one client; do not run all three commands as three purchases. Each output
must be new. An empty/partial output after failure is not a completed receipt.
Recover with the unchanged request file and a new output destination. TypeScript
and Python return `{status, retry_after, data}` for JSON/SSE; audio downloads are
raw MP3 bytes. cURL saves headers, HTTP status and raw bytes. For cURL SSE, use
`parse_stream` from `conversation.py` to verify the complete stream before acting:

```sh
python -c 'from pathlib import Path; from conversation import parse_stream; print(parse_stream(Path("opening-response/response.raw").read_text(encoding="utf-8"))["request_id"])'
```

For closeout, pass `True` as `parse_stream`'s second argument. No `[DONE]` sentinel
is used. HTTP 200 and provisional text alone do not establish successful billing
or saved content. The clients verify stream/session/receipt identity and safety
presence, preserve the final receipt, and refuse truncated completion.

## Complete practice flow

Keep the scope fields above in each file. These are the remaining fields/bodies;
replace IDs with catalog or prior-response values. Key scopes are cumulative:
`chat` for mutations, `sessions:read` for saved reads and audio continuation,
`data:export` for export, `data:erase` for user erasure.

| Step | Method and path | Body or query | Cost / completion |
| --- | --- | --- | --- |
| Catalog | `GET /conversation/catalog` | No body | Free; select actual room/persona IDs. |
| Prepare | `POST /conversation/sessions` | Setup example below | Free; persist `session_id` and `content_release`. |
| Opening | `POST /chat` | `{"session_id":"SESSION","conversation":{"operation":"opening"}}` | $0.03 at dispatch; no learner prompt. |
| Ordinary turn | `POST /chat` | Turn example below | $0.03 at dispatch, subject to observed-precontent refund. |
| Recover | `GET /requests/REQUEST_ID` | No body | No new generation; wait for committed outcome or retry exact original POST. |
| Read consent | `GET /conversation/sessions/SESSION/consent` | No body | Free. |
| Change consent | `POST /conversation/sessions/SESSION/consent` | `{"action":"soften"}` | Explicit revision; use the documented action enum. |
| Reopen | `GET /conversation/sessions/SESSION` | No body | Saved scope, state and current controls. |
| Playback | `GET /conversation/sessions/SESSION/playback` | `"query":{"limit":25}` | Free; follow `next_cursor`. |
| Close | `POST /sessions/SESSION/end` | `{"operation_id":"SAVED-UUID"}` | Billable generated closeout on commit; require `session_ended`. |
| Debrief | `GET /conversation/sessions/SESSION/debrief` | No body | Free; observe availability, without inventing a score. |
| Insights | `GET /conversation/insights` | No body | Free; recorded practice, not diagnoses. |
| Session list | `GET /conversation/sessions` | `"query":{"limit":25}` | Follow every opaque cursor without changing scope. |

An authored setup body:

```json
{"setup":{"kind":"room","room_id":"CATALOG-ROOM","mode":"guided","participants":[{"persona_id":"CATALOG-PERSONA"}]},"controls":{"cast_language":"en-US","cast_speech":"natural","challenge":"supportive","emotional_heat":"calm"}}
```

Open practice uses `{"kind":"open","mode":"open_practice","cast_shape":"one_on_one","participants":[...] ,"topic":"A fictional team handoff"}`.
For multiple participants, choose `cast_shape:"room"`; use one to five supported
personas and the catalog's room constraints. Topic/intensive consent is explicit;
see the schemas before choosing a sensitive scenario. The practice type determines
which setup fields are accepted. Do not send consumer/private prompt documents.

An ordinary turn body:

```json
{"session_id":"SESSION","prompt":"What would help us agree on the next step?","conversation":{"operation":"turn","controls":{"challenge":"realistic","emotional_heat":"calm"}}}
```

Omitted controls retain the last saved value. The five controls are cast language,
speech style, challenge, emotional heat and coaching lens. Coach ordinarily uses
English. No fixed session turn cap applies. Complete request/output engineering
bounds are enforced server-side; qualified public allowances remain a release gate.
Emotional heat `raw` is accepted in turn controls only, as the end user's own choice.

Each `conversation_turn` carries `characters`: per cast member, `expression` (16
states), `expression_group` (five codes, the simpler set) and `presence`. They are
optional to use, for portraits or seat state; see `CONVERSATION_API.md`.

## Optional voice on an enabled release

1. Explicitly request `POST /conversation/voice` with
   `{"session_id":"SESSION","turn_id":"SAVED-TURN","operation":"generate"}`.
   Persist the returned `audio_id` and original request. Only saved cast/Coach text
   can be spoken; arbitrary TTS text is not accepted.
2. On status 202/pending, respect `Retry-After`. Each explicit
   `POST /conversation/audio/AUDIO/continue` with `{}` advances at most one segment
   of the existing purchase. Do not call `voice` again with a new purchase ID to
   recover a pending generation. Stop on ready, failed or expired.
3. Read `GET /conversation/audio/AUDIO`, or discover generations using
   `GET /conversation/sessions/SESSION/audio` and its pagination cursor. Reads do
   not continue generation. Fetch a returned segment only through its authenticated
   `/conversation/audio/AUDIO/segments/SEGMENT` path on the same assigned API host.
4. The whole successful saved generation costs $0.05, including every participating
   voice and spoken Coach. Replay is free for 24 hours from creation, with earlier
   deletion. Balance is required; there is no audio grace. A failed generation has
   no audio customer charge. Internal characters do not change the customer unit.
5. Explicit regeneration uses a **new** saved intent and
   `{"session_id":"SESSION","turn_id":"SAVED-TURN","operation":"regenerate","replaces_audio_id":"AUDIO"}`.
   That is a new purchase. Text plus one successful audio generation totals $0.08.

## Privacy and deletion

`POST /conversation/data/export` uses an empty body and `"query":{"limit":25}`.
Follow `next_cursor` in subsequent query parameters until null, preserving scope.
Do not put `operation_id` or pagination in its body. Each
page returns saved setup, turns, preferences, consent, practice memory and audio
metadata; download still-available audio separately before its expiry.

`DELETE /sessions/SESSION` with `{"operation_id":"SAVED-UUID"}` deletes a session.
`POST /data/erase` with the same body shape erases the scoped API user's practice
data. These are explicit destructive choices, separate from closeout. Content
access is revoked immediately; a pending 202 receipt means audio cleanup is not
confirmed. Retry the identical lifecycle operation until `status:"completed"`.
Financial records remain. Live text retention is 180 days since activity; test
retention is 30 days. Reads do not extend retention; audio uses its own fixed clock.

The console's Sandbox namespace is separate from these API examples. Never add an
operator's internal namespace prefix to an API end-user identifier.

## Errors and release gates

401/403 require correcting identity, assignment or scope; 402 concerns balance or
budget; 409 can mean pending work or a stale revision; 422 can mean reused
idempotency identity with changed input; 429 requires the supplied retry guidance.
An unavailable or interrupted response remains uncertain. Resolve the original
request before a fresh purchase. Do not add automatic provider retries.

Public legal/funding, content/voice rights, measured allowances, supported versions,
further paid qualification and exact release approval remain outstanding. These
examples are integration drafts, not a release or testing-budget approval.
