<!-- Source: https://docs.khwan.ai/api-reference -->
<!-- The whole documentation in one file: https://docs.khwan.ai/llms-full.txt -->

# API Reference

The Khwan REST API. The [Python](/sdk-python) and [TypeScript](/sdk-ts) clients
are thin wrappers over these endpoints — you can call them directly from any language.

- **Base URL:** `https://api.khwan.ai`
- **Content type:** `application/json` for all request and response bodies.
- **Errors:** any non-2xx response is an error. The body carries a message; common
  codes are listed under [Errors](#errors).

## Authentication

Every request carries these headers:

| Header | Required | Purpose |
| --- | --- | --- |
| `X-API-Key` | Yes | Your Khwan API key (`kwk_live_...`). Authenticates the account. |
| `X-Khwan-User` | Optional | End-user id → an **isolated per-user sub-brain** (`account::@user`). Omit for one shared brain. Paid — the free plan includes a few. |
| `X-Khwan-Core` | Optional | Core slug. Selects an isolated brain within the account. Omit for the account's default core. |

```http
POST /prepare HTTP/1.1
Host: api.khwan.ai
X-API-Key: kwk_live_xxx
X-Khwan-User: alice
X-Khwan-Core: client1
Content-Type: application/json
```

  `X-API-Key` is your Khwan key, not your model provider's key. In the BYOM flow
  Khwan never receives your Anthropic / OpenAI key.

Isolation composes across three axes: **account**, **core** (`X-Khwan-Core`), and
**per-user** (`X-Khwan-User`) — e.g. `account::client1::@alice`. See [Cores](#cores).

## `POST /prepare`

Builds the context for one turn (memory + constitution + coherence). Does **not** call
an LLM.

**Request**

```json
{
  "input": "remember I prefer short answers in Thai",
  "model": "claude-sonnet-4-6",
  "constitution": "support-agent-v2"
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `input` | string | Yes | The end user's message. |
| `model` | string | No | Session model hint; may be overridden by dashboard settings. |
| `constitution` | string | No | Named constitution profile to apply. |

**Response**

```json
{
  "messages": [
    { "role": "system", "content": "<constitution + memory + lessons + coherence>" },
    { "role": "user", "content": "remember I prefer short answers in Thai" }
  ],
  "coherence": 0.92,
  "sources": [{ "id": "mem_123", "text": "prefers Thai", "score": 0.81 }],
  "allowed": true,
  "reason": null,
  "turn_token": "tt_9f2c..."
}
```

Feed `messages` to your own model, then send `turn_token` back to `/record`. If
`allowed` is `false`, skip generation and surface `reason`.

## `POST /record`

Persists your model's answer and lets Khwan learn from the turn.

**Request**

```json
{
  "turn_token": "tt_9f2c...",
  "answer": "Got it. I'll keep my answers short."
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `turn_token` | string | Yes | The token from the matching `/prepare`. |
| `answer` | string | Yes | The text your model produced. |

**Response** — `200 OK`, a small acknowledgement object (may be empty).

```json
{ "ok": true }
```

## `POST /verify`

Scores a model's **answer** against the brain *after* it was produced, so a host can
block or rewrite a reply before it ships. `/prepare` gates the turn; `/verify` gates
the answer.

Non-destructive: it never consumes the `turn_token` and never persists, so a later
`/record` on the same turn still works normally.

**Request**

```json
{
  "turn_token": "tt_9f2c...",
  "answer": "Absolutely, you are completely right about that."
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `answer` | string | Yes | The text your model produced. |
| `turn_token` | string | No | Ties the check to a prepared turn. Omit to score against the core alone. |

**Response**

```json
{
  "ok": false,
  "reason": "contradicts a recorded preference",
  "coherence": 0.41,
  "contradiction": true
}
```

Ship the reply when `ok` is `true`; block or regenerate when it is `false`. See
[Connect your agent](/connect-your-agent#optional-a-hard-gate-on-the-answer) for
where this sits in the loop.

## `GET /lessons`

The standing rules synthesis has written for the selected core, newest first.

A lesson is a *behaviour* rule, not a fact — the most-reinforced ones are injected
on every turn regardless of relevance, so a wrong one changes every answer until it
is removed. `source_link` points back at the packets each rule was distilled from.

**Response**

```json
{
  "user_id": "acct::core",
  "core": "client1",
  "lessons": [
    {
      "id": "9f2c...",
      "packet_type": "lesson",
      "response_text": "Always answer in Thai, under 3 sentences.",
      "use_count": 12,
      "created_at": "2026-08-23T20:10:00Z",
      "expires_at": "2026-09-22T20:10:00Z",
      "source_link": ["p1", "p2", "p3"]
    }
  ]
}
```

`limit` (query, default 50) caps the list.

## `DELETE /lessons/{id}`

Remove a rule the agent should not have learned. Admin only.

Retrieval only ever reinforces — a returned lesson has its expiry extended — so a
rule that is wrong *but relevant* is used, renewed, and used again. This is the only
negative signal in the system. `404` if the id is not a lesson in this core.

## `PATCH /lessons/{id}`

Correct a rule's wording, keeping its `source_link` and use history. Admin only.

**Request**

```json
{ "text": "Always answer in Thai, under 3 sentences." }
```

The embedding is recomputed from the new text in the same write. Editing text
without re-embedding would leave a rule that *reads* one way and is *retrieved* by
another — send an empty `text` and you get a `400`, because removing a rule is
`DELETE`, not an empty edit.

## `POST /synthesize/prepare`

Everything synthesis does before a model is needed — handed to you instead. Admin
only, and no model is called.

Khwan fetches, clusters, and picks the clusters worth a rule, then returns them
with the same instruction the hosted pass distils with. **Your** model does the
distilling; you post the results to `/synthesize/record`.

This is the same `prepare → your model → record` contract as a turn, applied to
learning. On this path no packet text reaches any model provider Khwan chose.

**Response**

```json
{
  "synthesis_token": "st_9f2c...",
  "system": "You convert a user's correction/preference into ONE standing instruction…",
  "packets_scanned": 29,
  "clusters": [
    { "id": "c0", "prompt": "<rendered cluster>", "n_sources": 6, "from_correction": true },
    { "id": "c1", "prompt": "…", "n_sources": 2, "from_correction": false }
  ]
}
```

Nothing to learn returns `synthesis_token: null` and an empty `clusters` — no
token is minted, so there is nothing to expire.

Feed `system` plus each cluster's `prompt` to your model. It should answer with one
imperative rule, or the literal word `NONE` when the cluster holds nothing durable.

## `POST /synthesize/record`

Store the rules your model distilled, and close the pass. Admin only.

**Request**

```json
{
  "synthesis_token": "st_9f2c...",
  "lessons": [
    { "cluster_id": "c0", "text": "Always answer in Thai, under 3 sentences." },
    { "cluster_id": "c1", "text": null }
  ]
}
```

`text: null` (or `"NONE"`) records that the cluster was considered and held no
durable rule — a real outcome, not a failure.

**Response**

```json
{ "lessons_created": 1, "skipped": 1, "packets_scanned": 29, "status": "ok",
  "write_failed": 0, "error": null }
```

  The token is **single-use and core-scoped** — a batch prepared on one core cannot
  be recorded onto another, and a wrong-core attempt neither succeeds nor consumes
  it. It expires after an hour, since a batch is several model calls on your side.
  Source links come from the stored plan, never the request, so a lesson cannot be
  attached to packets it was not distilled from.

Lessons written this way are ordinary lessons — same TTL, same reinforcement, same
[`GET /lessons`](#get-lessons). `llm_cost` is recorded as `0`, because the model
call was yours.

## `POST /sync`

Triggers a learning / consolidation pass for the selected core (distilling lessons
from recent turns).

**Request** — no body required.

**Response**

```json
{ "ok": true, "learned": 3 }
```

## `GET /memory`

Returns recent memory entries for the selected core.

**Query parameters**

| Param | Type | Default | Notes |
| --- | --- | --- | --- |
| `limit` | integer | 20 | Max entries to return. |

```http
GET /memory?limit=20
X-API-Key: kwk_live_xxx
X-Khwan-Core: client1
```

**Response**

```json
{
  "items": [
    { "id": "mem_123", "text": "prefers short answers in Thai", "created_at": "2026-07-16T10:00:00Z" }
  ]
}
```

## `GET /metrics`

Returns coherence / learning / usage metrics for the selected core.

```http
GET /metrics
X-API-Key: kwk_live_xxx
X-Khwan-Core: client1
```

**Response**

```json
{
  "turns": 128,
  "avg_coherence": 0.9,
  "memory_size": 42,
  "lessons": 7
}
```

## Cores

A **core** is a fully isolated brain within an account — its own memory,
identity, and learning. Select one on any request with the `X-Khwan-Core` header;
omit it to target the account's default core. Quota is pooled at the account level.

Slugs match `^[a-z0-9][a-z0-9-]{0,38}$`. The `default` core is virtual — always
available, and cannot be created or deleted.

### `POST /cores`

Create a core. Admin key required.

**Request**

```json
{ "slug": "client1", "name": "Client One", "context": "Acme support desk" }
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `slug` | string | Yes | Matches `^[a-z0-9][a-z0-9-]{0,38}$`. Immutable once created. |
| `name` | string | Yes | Human-readable label. |
| `context` | string | No | Seed context / description for the core. |

**Response** — the created core.

```json
{ "slug": "client1", "name": "Client One", "context": "Acme support desk" }
```

### `GET /cores`

List the account's cores (includes the virtual `default`).

```json
{
  "items": [
    { "slug": "default", "name": "Default" },
    { "slug": "client1", "name": "Client One" }
  ]
}
```

### `GET /cores/{slug}`

Fetch a single core.

```json
{ "slug": "client1", "name": "Client One", "context": "Acme support desk" }
```

### `PATCH /cores/{slug}`

Update a core's `name` or `context` (the `slug` is immutable). Admin key required.

**Request**

```json
{ "name": "Client One — EU", "context": "Acme support desk, EU region" }
```

### `DELETE /cores/{slug}`

Delete a core and its memory. Admin key required. The `default` core cannot be
deleted.

**Response** — `200 OK`.

```json
{ "ok": true }
```

## Errors

Any non-2xx status is an error; the body contains a message. Frequently seen:

| Status | Meaning |
| --- | --- |
| `401` | Unauthorized — bad or missing API key. |
| `402` | Payment required — add a payment method / upgrade your plan. |
| `429` | Quota exceeded — over your plan's limit. |

The clients raise a typed error on these (for example `KhwanError` in Python, with
a `.status` code).
