> ## 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.

# API Introduction

> Base URLs, authentication, common headers, pagination, error handling, and rate limiting for the KaireonAI REST API.

## Base URLs

| Environment | Base URL                                  |
| ----------- | ----------------------------------------- |
| Playground  | `https://playground.kaireonai.com/api/v1` |
| Local dev   | `http://localhost:3000/api/v1`            |

All paths in this reference are relative to the base URL. For example, `POST /recommend` means `POST https://playground.kaireonai.com/api/v1/recommend`.

## Authentication

Every API request must identify a tenant. There are two ways to do that.

### The two API planes

The API is split into a **data plane** and a **control plane**:

* **Data plane** — exactly `POST /recommend`, `POST /respond`, `POST /respond/bulk`, and `POST /capture` (a legacy alias of `/respond`). This is the runtime decisioning loop your own systems call machine-to-machine, and it is what any `krn_` API key can reach.
* **Control plane** — everything else in this reference: all management CRUD (schemas, offers, categories, channels, creatives, decision flows, decisioning gates, connectors, pipelines, segments, models, reports, settings, API keys, …). Control-plane endpoints are first-party only — they require a browser session, an MCP connection, or an API key minted with the `control-plane` scope (admin-only to mint; see [API Keys](/api-reference/api-keys)).

A key without the required access gets an HTTP `403`:

```json theme={null}
{
  "title": "Forbidden",
  "detail": "This API key is scoped to the data plane (recommend / respond). Management endpoints require a first-party session, an MCP connection, or an API key minted with the \"control-plane\" scope."
}
```

<Warning>
  **Migration note:** as of the control-plane / data-plane split, existing API
  keys with no explicit scopes are now **data-plane-only** (recommend/respond).
  To manage resources programmatically, mint a new key with the
  `control-plane` scope.
</Warning>

### API Key (recommended for integrations)

Pass your key in the `X-API-Key` header. Every key starts with the `krn_`
prefix and is bound to the tenant that created it:

```bash theme={null}
curl -X POST https://playground.kaireonai.com/api/v1/recommend \
  -H "Content-Type: application/json" \
  -H "X-API-Key: krn_your_api_key" \
  -d '{"customerId": "CUST001"}'
```

Generate API keys in **Settings > API Keys**. There is **no** `Authorization:
Bearer` scheme — a `Bearer` token is rejected with `401`.

Because the key already identifies the tenant, `X-Tenant-Id` is **optional**
on API-key requests: when both are sent, the key's tenant wins and the header
is ignored (this prevents tenant spoofing). You may still include
`X-Tenant-Id` for readability.

<Warning>
  State-changing requests (`POST`/`PUT`/`PATCH`/`DELETE`) that use an API key
  must also send `Content-Type: application/json` (or an `X-Requested-With`
  header). This is the CSRF guard; a state-changing API-key request without
  either is rejected with `403`.
</Warning>

### Session Cookie

When using the KaireonAI platform UI, requests authenticate via session cookie automatically. The tenant is resolved from the JWT session. No additional headers are needed.

<Warning>
  Keep your API key secret. Never expose it in client-side code or public repositories.
</Warning>

## Common Headers

| Header            | Required               | Description                                                                                                                                                                 |
| ----------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-API-Key`       | Yes (for integrations) | Your API key. Must start with `krn_`. Identifies both the caller and the tenant.                                                                                            |
| `Content-Type`    | Yes (state-changing)   | Must be `application/json`. Also satisfies the CSRF guard for API-key `POST`/`PUT`/`PATCH`/`DELETE`.                                                                        |
| `X-Tenant-Id`     | Optional               | Tenant identifier. Ignored on API-key requests (the key's tenant wins); resolved from the session JWT for UI requests.                                                      |
| `Idempotency-Key` | Conditional            | Honored by the outcome-recording endpoints (Respond) to prevent double-counting. Can also be sent in the request body as `idempotencyKey`. See [Idempotency](#idempotency). |

## Idempotency

Idempotency is enforced **only on the outcome-recording endpoints**, where a
retried delivery must never double-count a reward:

| Endpoint                                            | `Idempotency-Key`                                                                                                                                                                     |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /respond`                                     | **Required** (header `Idempotency-Key` or body `idempotencyKey`). A retry with the same key returns the original `already_recorded` result — no second outcome row, no double reward. |
| `POST /respond/bulk`                                | Per-item `idempotencyKey` (defaulted from the item's fields when omitted).                                                                                                            |
| Shopify outcome webhooks (`/api/shopify/…/outcome`) | Deduped by `idempotencyKey` on the same DB guard.                                                                                                                                     |

Deduplication is enforced by a unique database constraint on the interaction
row (`(tenantId, idempotencyKey)`), not merely by a cache — even two concurrent
retries collapse to a single recorded outcome.

<Warning>
  **Every other mutating endpoint is NOT idempotent by design.** The management
  (control-plane) CRUD endpoints — create / update / delete for schemas, offers,
  categories, channels, creatives, decision flows, connectors, pipelines,
  segments, models, settings, and so on — do **not** honor `Idempotency-Key`. A
  retried `POST`/`PUT`/`PATCH`/`DELETE` on those routes may create a duplicate or
  re-apply the change. Make retries safe on the caller side (check-then-write, or
  rely on a natural unique key). Broadening idempotency to more endpoints is on
  the roadmap, not a current guarantee.
</Warning>

## Pagination

List endpoints that support pagination return a cursor-based response. The
page rows are in `data`; the paging state is nested under `pagination`:

```json theme={null}
{
  "data": [ ... ],
  "pagination": {
    "limit": 50,
    "cursor": "453b0424-b42e-4b7c-be4a-5fa908cf7751",
    "hasMore": true,
    "total": 247
  }
}
```

`pagination.cursor` is the **next** cursor — pass it back as the `cursor`
query parameter to fetch the following page. It is `null` on the last page.
`total` is included only when the endpoint runs a count query. Fetch the next
page with `?cursor=<pagination.cursor>`.

**Query parameters:**

| Parameter | Type    | Default | Description                                                              |
| --------- | ------- | ------- | ------------------------------------------------------------------------ |
| `limit`   | integer | 50      | Number of items to return (max 100)                                      |
| `cursor`  | string  | —       | The `pagination.cursor` from a previous response, to fetch the next page |

## Error Response Format

All errors follow a consistent structure:

```json theme={null}
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "customerId is required",
    "status": 400,
    "recommendationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "timestamp": "2026-03-16T12:00:00.000Z"
  }
}
```

Validation errors from Zod schemas return a combined detail string:

```json theme={null}
{
  "title": "Validation error",
  "detail": "customerId: Required; outcome: outcome (or interactionType) is required"
}
```

## Status Codes

| Code  | Meaning                                                            |
| ----- | ------------------------------------------------------------------ |
| `200` | Success                                                            |
| `201` | Created (new resource)                                             |
| `204` | No content (successful delete)                                     |
| `400` | Bad request — invalid or missing fields                            |
| `401` | Unauthorized — missing or invalid API key / session                |
| `403` | Forbidden — insufficient permissions for the requested action      |
| `404` | Not found — resource does not exist or belongs to another tenant   |
| `409` | Conflict — resource already exists (e.g., duplicate key)           |
| `415` | Unsupported Media Type — `Content-Type` must be `application/json` |
| `429` | Rate limited — wait and retry after the `Retry-After` interval     |
| `500` | Internal server error — an unexpected error occurred               |

## Rate Limiting

API requests are rate-limited per tenant on a sliding window basis. The default limit is **1,000 requests per 60-second window**.

When a request is rate-limited, the response includes these headers:

| Header                  | Description                                    |
| ----------------------- | ---------------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests remaining in the current window       |
| `Retry-After`           | Seconds to wait before retrying                |

**Example 429 response:**

```json theme={null}
{
  "error": "Too Many Requests",
  "retryAfter": 42
}
```

When you receive a `429`, wait for the number of seconds in the `Retry-After` header before retrying.

## Next Steps

<CardGroup cols={2}>
  <Card title="Recommend API" icon="wand-magic-sparkles" href="/api-reference/recommend">
    Get personalized next-best-action recommendations for a customer.
  </Card>

  <Card title="Respond API" icon="reply" href="/api-reference/respond">
    Record impressions, clicks, conversions, and other outcomes.
  </Card>

  <Card title="API Tutorial" icon="code" href="/tutorials/api-tutorial">
    End-to-end walkthrough with advanced features like Decision Flows and computed values.
  </Card>

  <Card title="MCP Server" icon="plug" href="/integrations/mcp">
    Use the Recommend and Respond APIs as MCP tools from AI agents.
  </Card>
</CardGroup>
