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

# Respond API

> Record customer interactions and outcomes (impressions, clicks, conversions) against delivered recommendations.

<Frame caption="The decision flow whose outcomes /respond records.">
  <img src="https://mintcdn.com/kaireonai/l-jsUQlUEuA3B6hG/images/screenshots/decision-flows-list.png?fit=max&auto=format&n=l-jsUQlUEuA3B6hG&q=85&s=d5abc231dda18ee0ad09d0534789d15b" alt="Decision Flows list view in the Kaireon Studio" width="1440" height="900" data-path="images/screenshots/decision-flows-list.png" />
</Frame>

## POST /api/v1/respond

Records the outcome of a recommendation delivered via the [Recommend API](/api-reference/recommend). Outcomes feed into behavioral metrics, adaptive model training, experiment analysis, and attribution.

<Note>
  **Data plane.** This endpoint is part of the public data plane — the runtime
  decisioning loop (`/recommend`, `/respond`, `/respond/bulk`, `/capture`) —
  and is callable with any `krn_` API key, including default keys minted
  without scopes. Management endpoints, by contrast, require control-plane
  access (see [API Keys](/api-reference/api-keys)).
</Note>

### Minimal Request (Recommended)

The smallest way to record an outcome — 5 fields. The system resolves the offer, creative, and channel from the recommendation record. `idempotencyKey` is **required** on every call (a request without it is rejected with `400`).

```json theme={null}
{
  "customerId": "CUST001",
  "recommendationId": "rec_7f3a2b1c-9d4e-5f6a-8b7c-0d1e2f3a4b5c",
  "rank": 1,
  "outcome": "click",
  "idempotencyKey": "click-cust001-rank1-1710590400"
}
```

### Full Request (all optional fields)

```json theme={null}
{
  "customerId": "CUST001",
  "recommendationId": "rec_7f3a2b1c-9d4e-5f6a-8b7c-0d1e2f3a4b5c",
  "rank": 1,
  "outcome": "click",
  "idempotencyKey": "click-cust001-offer001-1710590400",
  "creativeId": "creative_001",
  "offerId": "offer_001",
  "channelId": "channel_email",
  "direction": "outbound",
  "context": {
    "source": "email_campaign_q1",
    "utm_medium": "email"
  },
  "outcomeDetails": {
    "product_sku": "SKU-12345"
  },
  "timestamp": "2026-03-16T14:35:00.000Z",
  "conversionValue": 149.99
}
```

### Field Reference

| Field              | Type    | Required | Description                                                                                                                                                                                                                                                                                      |
| ------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `customerId`       | string  | Yes      | The customer who interacted with the recommendation                                                                                                                                                                                                                                              |
| `recommendationId` | string  | Yes\*    | The `recommendationId` from the Recommend API response. Used with `rank` to resolve the offer automatically                                                                                                                                                                                      |
| `rank`             | integer | Yes\*    | Which offer from the recommendation (1-based). Combined with `recommendationId` to look up the offer, creative, and channel                                                                                                                                                                      |
| `outcome`          | string  | Yes      | The outcome type key (e.g., `"click"`, `"accept"`, `"dismiss"`, `"convert"`, `"renewed"`, `"unsubscribed"`). Must match a registered Outcome Type                                                                                                                                                |
| `creativeId`       | string  | No       | Explicit Creative ID. Only needed if NOT using `recommendationId` + `rank`                                                                                                                                                                                                                       |
| `idempotencyKey`   | string  | **Yes**  | **Required.** Unique key to prevent double-counting. Returns 400 if missing. Can also be sent as `Idempotency-Key` header                                                                                                                                                                        |
| `interactionId`    | string  | No       | Legacy field — use `recommendationId` instead                                                                                                                                                                                                                                                    |
| `direction`        | string  | No       | Direction of the interaction: `"inbound"` or `"outbound"`. Default is `"inbound"` for most outcome types. For impression-category outcomes (e.g., `impression`, `delivery`), the default is `"outbound"` since those represent platform-initiated contacts                                       |
| `offerId`          | string  | No       | Offer ID. If omitted, resolved automatically from the creative's parent offer                                                                                                                                                                                                                    |
| `channelId`        | string  | No       | Channel ID. A value passed in the body is used as-is; when omitted it is resolved from the creative's channel (when `creativeId` is provided). Resolving the channel keeps interaction summaries and contact-policy frequency caps attributed to the correct channel.                            |
| `placementId`      | string  | No       | Placement ID. If omitted, resolved from the creative                                                                                                                                                                                                                                             |
| `rank`             | integer | No       | The position at which the offer was displayed (1-indexed)                                                                                                                                                                                                                                        |
| `context`          | object  | No       | Arbitrary context metadata (campaign source, UTM params, etc.)                                                                                                                                                                                                                                   |
| `outcomeDetails`   | object  | No       | Structured details about the outcome (e.g., product SKU, plan selected)                                                                                                                                                                                                                          |
| `timestamp`        | string  | No       | ISO 8601 timestamp of the interaction. Defaults to the current server time                                                                                                                                                                                                                       |
| `conversionValue`  | number  | No       | Monetary value of the conversion (used for attribution and revenue reporting)                                                                                                                                                                                                                    |
| `attributes`       | object  | No       | Additional attributes (device, browser, etc.). Consumed live by the incremental learners AND persisted into the interaction row's `context`, so batch `/train` extractors — which read the row's `context` — see the same features the online path saw. Explicit `context` keys win on collision |
| `channel`          | string  | No       | Channel name string (stored in context)                                                                                                                                                                                                                                                          |
| `placement`        | string  | No       | Placement name string (stored in context)                                                                                                                                                                                                                                                        |
| `responseTime`     | number  | No       | Time in milliseconds the customer took to respond                                                                                                                                                                                                                                                |
| `deviceType`       | string  | No       | Device type string (stored in context)                                                                                                                                                                                                                                                           |

<Note>
  Fields marked **Yes**\* require at least one resolution path. You must provide either `creativeId` OR `recommendationId` + `rank` to identify the offer. The field `outcome` is required (the legacy alias `interactionType` is also accepted).
</Note>

<Note>
  Prior to this fix, `channelId` was only resolved when `offerId` was missing, causing per-channel frequency caps to be silently broken.
</Note>

### Attribution — how `recommendationId` + `rank` resolves the offer

When `recommendationId` and `rank` are supplied (without a direct `creativeId`), the route looks up the original recommendation through a three-tier ladder. Tiers run in order; the first match wins.

| Tier                       | How it matches                                                                         | When it applies                                                                                                                                                                                                                                                        |
| -------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1 — column match**       | `recommendationId` column = supplied value AND `rank` = supplied value                 | Recommendation rows written after the 2026-06-07 fix. The dedicated column index makes this the fastest and most precise path; a concurrent call for the same customer at the same rank cannot cross-attribute.                                                        |
| **2 — legacy JSON match**  | `response->>'interactionId'` = supplied `recommendationId` AND `rank` = supplied value | Recommendation/impression rows written before the fix, when the interaction ID was stored only inside the `response` JSONB. Rank is still filtered — the stored rank matches exactly what `/recommend` returned, so keeping the clause cannot drop a legitimate match. |
| **3 — rank-only fallback** | Most recent `recommendation` or `impression` row for `customerId + rank`               | Reached only when tiers 1 and 2 both miss (e.g. a row exists but carries no `recommendationId` in either the column or the JSONB). Callers that never send `recommendationId`, and callers that supply `creativeId` directly, skip all three tiers.                    |

If none of the three tiers resolve a row, the route returns `400 No recommendation found for customer=… rank=…`.

**Limitation:** multi-placement `/recommend` responses return a `recommendationId` in the response body but write no `interaction_history` rows (pre-existing behaviour). Precise tier-1 attribution therefore applies to single-flow recommendations and auto-impressions only. Multi-placement callers should supply `creativeId` directly.

### Idempotency

Every call to the Respond API requires an idempotency key to prevent duplicate outcome recording. You can provide it in two ways:

1. **Request body:** `"idempotencyKey": "unique-key-here"`
2. **HTTP header:** `Idempotency-Key: unique-key-here`

If both are provided, the body value takes precedence.

When a duplicate key is detected, the API returns the original record with `"status": "already_recorded"` and a `200` status code (not `201`).

<Note>
  **`recorded_without_adaptation` (200).** A *positive* outcome for a `(customer, offer, creative)` combination that has no matching recommendation and no recent impression (a 30-day lookback) is still written, but adaptive-model training is skipped so a phantom positive can't inflate attribution past delivery. In that case the response is `{ "ok": true, "status": "recorded_without_adaptation", "interactionId": "…", "warning": "…" }` with a `200` status. Impression/neutral/negative outcomes are unaffected.

  `GET /api/v1/recommend` now writes the same recommendation and impression rows as POST and returns `interactionId`/`recommendationId`, so outcomes for GET-served recommendations attribute normally — GET-driven traffic no longer lands in `recorded_without_adaptation` when you close the loop with `recommendationId + rank`.
</Note>

### Model adaptations — attributed to the model that decided

Per-offer/category/channel/direction/global **model adaptations** (the propensity-learning rows the scorer reads back — see [Adaptive Learning](/ai-ml/adaptive-learning)) are written for **exactly one model: the model that produced the decision**. The target model is resolved from the delivery row's recorded `modelId`; if no delivery row for this `(customer, offer)` carries a model attribution, no adaptation rows are written at all, rather than fanning the outcome out to every active model. Models are isolated — one model's outcome stream can no longer shift another model's scores through shared adaptation cells.

Separately, per-outcome **incremental model-state updates** still apply to every active incremental-type model (`bayesian`, `thompson_bandit`, `epsilon_greedy`, `online_learner`), using the request's `attributes` as the feature bag. For bandits configured with `armScope: "offer_channel"`, the outcome's `channelId` selects the arm — the update lands on `<offerId>:<channelId>` so channel-specific stats accumulate separately (see [How Scoring Works §6c](/decisioning/how-scoring-works#6c-bandits-write-per-offer-or-per-offer-per-channel-state)).

### Response: New Record (201)

The response includes enriched names for easy display without additional lookups.

```json theme={null}
{
  "interactionId": "550e8400-e29b-41d4-a716-446655440000",
  "recommendationId": "c6184851-54f8-49df-88e3-0b11ed3b9fcb",
  "customerId": "CUST-00500",
  "outcome": "click",
  "classification": "positive",
  "rank": 4,
  "offerName": "Multi-Policy Bundle",
  "creativeName": "Multi-Policy Bundle — Email",
  "channelName": "Email Campaign",
  "categoryName": "Policy Renewal",
  "status": "recorded",
  "timestamp": "2026-03-16T14:35:00.000Z"
}
```

### Response: Duplicate (200)

When the same `idempotencyKey` is sent again:

```json theme={null}
{
  "interactionId": "550e8400-e29b-41d4-a716-446655440000",
  "recommendationId": "c6184851-54f8-49df-88e3-0b11ed3b9fcb",
  "customerId": "CUST-00500",
  "outcome": "click",
  "status": "already_recorded",
  "timestamp": "2026-03-16T14:35:00.000Z"
}
```

***

## Outcome Types

Outcomes must be registered in the platform before they can be recorded. Each outcome type has a classification that drives downstream behavior:

| Classification | Effect                                                                                  | Examples                                   |
| -------------- | --------------------------------------------------------------------------------------- | ------------------------------------------ |
| `positive`     | Increments positive counters, triggers attribution, updates adaptive models with reward | `click`, `accept`, `convert`, `purchase`   |
| `negative`     | Increments negative counters, updates models with penalty                               | `dismiss`, `not_interested`, `unsubscribe` |
| `neutral`      | Tracked but does not affect scoring                                                     | `impression`, `view`, `requested_info`     |

Register outcome types in **Studio > Outcome Types** or via `POST /api/v1/outcome-types`.

***

## Examples

### Record an Impression

```bash theme={null}
curl -X POST https://playground.kaireonai.com/api/v1/respond \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: my-tenant" \
  -H "X-API-Key: krn_your_api_key" \
  -d '{
    "customerId": "CUST001",
    "creativeId": "creative_001",
    "outcome": "impression",
    "idempotencyKey": "imp-cust001-creative001-20260316-1430",
    "interactionId": "550e8400-e29b-41d4-a716-446655440000",
    "rank": 1
  }'
```

### Record a Click

```bash theme={null}
curl -X POST https://playground.kaireonai.com/api/v1/respond \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: my-tenant" \
  -H "X-API-Key: krn_your_api_key" \
  -d '{
    "customerId": "CUST001",
    "creativeId": "creative_001",
    "outcome": "click",
    "idempotencyKey": "click-cust001-creative001-20260316-1435",
    "interactionId": "550e8400-e29b-41d4-a716-446655440000",
    "context": {
      "source": "email_hero_banner"
    }
  }'
```

### Record a Conversion with Value

```bash theme={null}
curl -X POST https://playground.kaireonai.com/api/v1/respond \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: my-tenant" \
  -H "X-API-Key: krn_your_api_key" \
  -d '{
    "customerId": "CUST001",
    "creativeId": "creative_001",
    "outcome": "convert",
    "idempotencyKey": "conv-cust001-offer001-20260316",
    "interactionId": "550e8400-e29b-41d4-a716-446655440000",
    "offerId": "offer_001",
    "conversionValue": 299.99,
    "outcomeDetails": {
      "plan": "platinum",
      "term_months": 12
    }
  }'
```

***

## Error Responses

| Status | Cause                                                                                                                                                                                                                                                     |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Missing required fields (`customerId`, `creativeId`/`treatmentId` or `recommendationId`+`rank`, `outcome`/`interactionType`, `idempotencyKey`), unknown outcome type, no recommendation found for the supplied `recommendationId`+`rank`, or invalid JSON |
| `401`  | Missing, invalid, revoked, or expired API key (the key must start with `krn_`)                                                                                                                                                                            |
| `403`  | The resolved tenant does not exist, or the CSRF guard rejected the request (an API-key `POST` without `Content-Type: application/json` or `X-Requested-With`)                                                                                             |
| `404`  | Creative not found or belongs to another tenant                                                                                                                                                                                                           |
| `415`  | `Content-Type` header is not `application/json`                                                                                                                                                                                                           |
| `429`  | Rate limit exceeded (1,000 requests per 60s per tenant)                                                                                                                                                                                                   |
| `500`  | Internal server error                                                                                                                                                                                                                                     |

**Example error (unknown outcome type):**

```json theme={null}
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Unknown outcome type: \"purchased\". Register it in Outcome Types first.",
    "status": 400,
    "recommendationId": "d4e5f678-90ab-cdef-1234-567890abcdef",
    "timestamp": "2026-03-16T14:35:00.000Z"
  }
}
```

See also: [Outcome Types](/studio/outcome-types) | [Behavioral Metrics](/studio/behavioral-metrics) | [API Tutorial](/tutorials/api-tutorial)
