> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.westiq.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.westiq.ai/_mcp/server.

# Enrichment

`POST /v1/enrichment` runs a deeper pass over a member's accumulated behavior to
surface newly-discovered preferences and genre-affinity shifts, with a
passenger-visible narrative. It is **gated**: a deterministic trigger decides
whether the deep pass is warranted *before* any model call, so most requests are
cheap and only some do real work.

[Session-scoped](/guides/sessions#the-x-session-id-header) — **requires the
`X-Session-Id` header**. Enrichment is **title-free by design**: you identify the
member, not a content batch. Request body max 8 KiB.

| Field        | Required | Rules                                                                                                                                 |
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `seatId`     | yes      | The member's seat.                                                                                                                    |
| `memberSlot` | no       | Which member on a shared seat. Omit for solo.                                                                                         |
| `manual`     | no       | `true` signals a manual trigger request. It is **one input to the gate, not a bypass** — the gate still decides. Defaults to `false`. |

```bash
curl -X POST "https://api.staging.westiq.ai/v1/enrichment" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-Session-Id: seat-12A-f3d9c1" \
  -H "Content-Type: application/json" \
  -d '{ "seatId": "12A", "manual": true }'
```

When the trigger fires, `applied` is `true` and the pass returns real content:

```json
{
  "schemaVersion": "1",
  "correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d",
  "discoveredPreferences": [
    { "category": "genre", "label": "Slow-burn thrillers", "confidence": 0.72 }
  ],
  "genreShifts": [
    { "fromGenre": "Action", "toGenre": "Thriller", "magnitude": 0.35 }
  ],
  "reasoningNarrative": "Lately you've been finishing tense, quieter thrillers…",
  "applied": true
}
```

* `discoveredPreferences[].confidence` and `genreShifts[].magnitude` are
  code-clamped to 0–1.
* `reasoningNarrative` is passenger-visible model free text.

## `applied: false` is a cost-free decline, not an error

> **Warning**
>
> When the trigger gate declines — the member is rate-limited, a pass is already
> in flight, or a session cap is reached — the response is a `200` with
> **`applied: false`**, **no model call made**, and empty content:
>
> ```json
> { "schemaVersion": "1", "correlationId": "req_…",
>   "discoveredPreferences": [], "genreShifts": [],
>   "reasoningNarrative": "", "applied": false }
> ```
>
> This is normal and costs nothing. Branch on `applied`: only merge results when
> it is `true`; an `applied: false` simply means "not now."

> **Note**
>
> Enrichment **never rewrites** the member's persona — the persona is immutable
> for the session. The refinement rides this response (and the affinity signals
> that [scored serve](/guides/catalog-serve#scored-serve) and
> [suggestions](/guides/suggestions) already read), not a persona mutation. The
> [`enrichment.ready` SSE frame](/guides/session-events#frame-types) is not yet
> published — read this HTTP response.

## Errors

Standard [envelope](/guides/errors): `400 session.unresolved`, `401` / `403`,
`422 request.validation`, `429 rate.limited` over the [per-client request-rate limit](/guides/errors#rate-limits) (honor the `Retry-After` header), `500`.
Note a declined trigger is **not** an error — it's a `200` with
`applied: false` (and the per-member trigger gate is a different mechanism
from the per-client rate limit).