> ## Documentation Index
> Fetch the complete documentation index at: https://docs.epode.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Customer context API

> The exact company and agent contracts for enrichment, retrieval, personalization, and outcomes.

Company endpoints use `Authorization: Bearer $EPODE_API_KEY`. Agent answer endpoints use only the short-lived
`aqr1_...` request handle returned for that interaction. The company key must never enter an agent response.

## Define product questions

By default a product shares Epode's built-in global question catalog. A product can instead define its own closed
catalog; as soon as one enabled question exists, it replaces the global catalog for that product's requests.
Every request stores an immutable snapshot of the questions it may ask, so editing or deleting a definition
never changes an in-flight or historical request.

`PUT /api/v2/enrichment/fields/{fieldKey}`

```json theme={null}
{
  "label": "Shopping occasion",
  "type": "customer_goal",
  "allowedValues": ["gift", "self_purchase", "replacement"],
  "targetedAdvertisingSafe": false,
  "operations": ["/api/recommendations"],
  "enabled": true
}
```

* `fieldKey` is a lowercase `namespace.name` pair such as `journey.occasion`; the `epode.*` namespace is
  reserved for built-in fields.
* `label` is the question topic Epode inserts into the customer permission prompt.
* `type` is a product-authored lowercase snake\_case category of 1–48 characters, such as `preference`,
  `constraint`, or `customer_goal`.
* `allowedValues` carries 1–32 distinct lowercase snake\_case values. Free-form text, arbitrary numbers, and
  sensitive categories are never accepted.
* `operations` optionally binds the field to specific business operations. Omit it to allow every operation. A
  request that names the field for an operation it is not bound to fails closed, and a request for an operation
  with no eligible fields creates no question at all.

`GET /api/v2/enrichment/fields` lists the product's definitions and whether the legacy global catalog is still
active. `DELETE /api/v2/enrichment/fields/{fieldKey}` removes a definition; existing requests keep their
snapshots.

## Request enrichment

`POST /api/v2/enrichment/requests`

```json theme={null}
{
  "interactionId": "018f1f2e-7b4a-7c12-9c8d-123456789abc",
  "operation": "/api/recommendations",
  "surface": "http_json",
  "statusCode": 200,
  "durationMs": 18,
  "sessionRef": "journey_42",
  "runtimeHint": "shopping-agent/1.2",
  "requestObservation": {
    "clientIp": "203.0.113.42",
    "method": "GET",
    "userAgent": "ExampleBrowser/1.0",
    "acceptLanguage": "en-US,en;q=0.9",
    "referrerOrigin": "https://shop.example",
    "secChUaPlatform": "\"macOS\"",
    "secChUaMobile": "?0"
  },
  "purpose": "product_personalization",
  "remember": true,
  "accountRef": "account_42",
  "userRef": "user_7",
  "anonymousRef": "visitor_99"
}
```

Identity references are optional; omit all of them for an ephemeral interaction. A new request is
`answer_ready` immediately: the submit action and field catalog arrive in the first response and no permission
question is ever asked. `declined` means an explicit opt-out is on record for this customer and purpose — the
consent decision endpoint still accepts explicit `declined` decisions (and `approved` for backward
compatibility), and a recorded no is always honored. `answered` and `no_relevant_context` mark completed
requests. The response contains only the currently allowed next action.
`targeted_advertising` is the other supported purpose and always requires its own separate permission; a
`product_personalization` grant is never reused for it.

`fieldKeys` optionally selects the exact fields this request may collect: one to eight keys drawn from the
product's effective catalog (its own definitions once it has any, otherwise the global catalog). The answer
schema then offers only them, and the backend rejects answers that name anything else. Retrying the same
interaction with a different selection conflicts.

`statusCode`, `durationMs`, `sessionRef`, and `runtimeHint` are optional same-call evidence. Framework adapters
set status and duration automatically. A company may add only a product-issued `sessionRef` and a bounded,
non-sensitive runtime label; agent/model arguments are not evidence.

Express and Fastify also populate `requestObservation` automatically from a fixed allowlist: the framework's
resolved peer IP, HTTP method, user agent, accepted language, referrer **origin**, and User-Agent Client Hints.
Proxy forwarding is used only when the host application has configured that framework to trust the proxy. The
adapter never copies cookies, credentials, bodies, full referrer URLs, query strings, or arbitrary headers.
MAC addresses are link-layer identifiers and are not available to an internet-facing HTTP server.

`surface` is exactly `http_json`, `html`, or `mcp`. Node adapters set it automatically. JSON and HTML begin as
unclassified opportunities; an MCP business-tool result is confirmed immediately by the same enrichment request,
so companies do not install a second observability wrapper.

The Node HTTP adapter rewrites agent action URLs to:

* `POST /_epode/v1/enrichment/consent`
* `POST /_epode/v1/enrichment/answers`

Those are fixed company-owned relay paths. They forward only the `aqr1_` handle and allowlisted body—never the
company key, product request, prompt, transcript, cookies, or arbitrary headers.

Both the initial response and the post-consent relay response preserve the backend's explicit
`stageInstruction`, optional `answerInstruction`, and exact `bodySchema` for the currently allowed action. Every
action returned by the relay is rewritten again, so an HTTP agent is never instructed to write to the Epode
origin.

## Submit an answer

`POST /api/v2/enrichment/answers` accepts:

```json theme={null}
{
  "status": "answered",
  "items": [
    {
      "key": "shopping.budget_band",
      "type": "constraint",
      "value": "50_150",
      "provenance": "agent_reports_user_statement",
      "confidence": 1,
      "remember": true,
      "expiresAt": "2026-09-01T00:00:00Z"
    }
  ]
}
```

`status` is `answered`, `declined`, or `no_relevant_context`. `items` contains at most eight entries and must be
empty for the latter two statuses. The action's `bodySchema.catalog` is the only accepted key, type, and value
vocabulary; unknown combinations fail closed. Epode generates the stored summary from that catalog instead of
storing agent-written prose. Remembered items require the matching grant. Requested expiry is clamped to the
product retention policy.

For a product-authored category, the catalog entry exposes that category as `questionType` while its `type`
remains the canonical compatibility class the answer must submit. This keeps in-flight requests safe across a
rolling API deploy; retrieved context still reports the product-authored category.

The catalog includes bounded, non-sensitive preferences for several product categories. Current examples include:

| Product category   | Keys                                                                     | Example values                                        |
| ------------------ | ------------------------------------------------------------------------ | ----------------------------------------------------- |
| Travel             | `travel.stay_style`, `travel.location_priority`, `travel.room_priority`  | `quiet_boutique`, `central_walkable`, `reliable_wifi` |
| Financial products | `finance.explanation_style`, `finance.liquidity_preference`              | `comparison_table`, `few_days`                        |
| Care navigation    | `care.communication_style`, `care.visit_mode`, `care.appointment_timing` | `plain_language`, `telehealth`, `weekday_evening`     |
| Education          | `education.learning_format`, `education.learning_level`                  | `project_based`, `intermediate`                       |

These preferences describe how the product should serve the customer. They are not permission to collect travel
documents, holdings, balances, income, creditworthiness, diagnoses, symptoms, medications, disabilities, or other
sensitive or free-form facts. Those fields are absent from the catalog and fail closed. All four category-specific
groups above are also marked `targetedAdvertisingSafe: false`.

For `targeted_advertising`, the backend also requires the selected catalog entry's
`targetedAdvertisingSafe` field to be `true`. For example, the bounded `interest.topic` preference accepts
non-sensitive values such as `outdoor_travel`; B2B company size, purchase intent, unknown audience labels, and
sensitive or free-form attributes fail closed even after advertising permission is approved.

## Retrieve permitted context

`POST /api/v2/customer-context`

```json theme={null}
{
  "userRef": "user_7",
  "anonymousRef": "visitor_99",
  "purpose": "product_personalization"
}
```

The response includes a `retrievalId`, identity level, context version, and active items with `signalId`,
provenance, allowed uses, remembered state, and expiry. It is read-only: unknown references do not create a
customer. Revoked, expired, unresolved, or wrong-purpose items are omitted.

For an ephemeral interaction, pass the original `interactionId` instead of an identity reference. The Node HTTP
adapter exposes this as `Epode-Context-Interaction` for the immediate retry and `customer.contextFor(request)`
validates and reads it. It is not durable identity and must not be reused for another operation or later visit.

## Record the decision

`POST /api/v2/personalization/decisions`

```json theme={null}
{
  "externalDecisionId": "recommendation_42",
  "contextRetrievalId": "retrieval_42",
  "signalIds": ["signal_1", "signal_2"],
  "variant": "customer-context-ranking-v1"
}
```

Every signal ID must be an exact subset of that retrieval. Identical retries are idempotent; changing a retry
returns `409`.

## Record the outcome

`POST /api/v2/personalization/outcomes`

```json theme={null}
{
  "externalOutcomeId": "order_42",
  "decisionId": "decision_42",
  "outcome": "conversion"
}
```

Outcome is `conversion`, `completion`, `engagement`, `dismissal`, or `abandonment`. Identical retries are
idempotent and conflicting retries return `409`.
