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

# Offers API

> Create, update, list, and delete offers (actions) in the KaireonAI platform.

<Note>
  **See also**: [Offers concept and configuration](/studio/offers) for what this API powers, when to call it, and how it is configured.
</Note>

<Frame caption="The Actions page where offers are managed.">
  <img src="https://mintcdn.com/kaireonai/l-jsUQlUEuA3B6hG/images/screenshots/offers-list.png?fit=max&auto=format&n=l-jsUQlUEuA3B6hG&q=85&s=3a892df20ee6082c5e2073ff05825835" alt="Actions page showing offers in the Studio module" width="1440" height="900" data-path="images/screenshots/offers-list.png" />
</Frame>

Offers are the core decisioning entity in KaireonAI. Each offer represents a recommendation that can be scored, ranked, and delivered to a customer through one or more channels.

All offers support **soft-delete** (a `deletedAt` timestamp is set instead of permanent removal), **version tracking** (the `version` field auto-increments on every update), and **audit logging** (before/after snapshots are recorded for every CRUD operation).

<Info>
  See the [Offers feature page](/studio/offers) for UI guidance and conceptual overview.
</Info>

## Base path

```
/api/v1/offers
```

***

## List offers

```
GET /api/v1/offers
```

Returns a paginated list of offers for the current tenant, ordered by creation date (newest first). Each offer includes its creatives, category, and sub-category relations. By default, soft-deleted offers are excluded.

### Query parameters

| Parameter        | Required | Type    | Description                                                              |
| ---------------- | -------- | ------- | ------------------------------------------------------------------------ |
| `limit`          | No       | integer | Maximum number of results to return. Default `50`, max `100`.            |
| `cursor`         | No       | string  | Cursor for keyset pagination. Pass the last `id` from the previous page. |
| `includeDeleted` | No       | string  | Set to `"true"` to include soft-deleted offers in the results.           |

### Response `200`

```json theme={null}
{
  "data": [
    {
      "id": "off_abc123",
      "tenantId": "t_001",
      "name": "Platinum Card Upgrade",
      "status": "active",
      "categoryId": "cat_01",
      "subCategoryId": "sub_01",
      "category": "acquisition",
      "group": "",
      "priority": 75,
      "businessValue": 50,
      "mandatory": false,
      "mandatoryReason": null,
      "mandatoryExpiresAt": null,
      "mandatoryApprovedBy": null,
      "emergencyExcluded": false,
      "description": "Premium card upgrade offer for high-value customers.",
      "shortDesc": "Upgrade to Platinum",
      "eligibility": {},
      "budget": { "maxPerDay": 500 },
      "schedule": {},
      "tags": ["credit-card", "premium"],
      "productType": "credit_card",
      "margin": null,
      "revenueValue": null,
      "metadata": {},
      "version": 1,
      "deletedAt": null,
      "creatives": [],
      "categoryRef": { "id": "cat_01", "name": "Acquisition" },
      "subCategoryRef": null,
      "createdAt": "2026-03-10T12:00:00.000Z",
      "updatedAt": "2026-03-12T09:30:00.000Z"
    }
  ],
  "pagination": {
    "limit": 25,
    "cursor": "off_xyz789",
    "hasMore": true,
    "total": 42
  }
}
```

### Error codes

| Code  | Reason                                       |
| ----- | -------------------------------------------- |
| `401` | Missing or invalid API key / session.        |
| `403` | Insufficient role.                           |
| `429` | Rate limit exceeded (2,000 requests / 60 s). |

***

## Create an offer

```
POST /api/v1/offers
```

Creates a new Offer. Triggers auto-assembly on any active Decision Flows.

### Request body

| Field                 | Required | Type            | Description                                                                                        |
| --------------------- | -------- | --------------- | -------------------------------------------------------------------------------------------------- |
| `name`                | **Yes**  | string (1-255)  | Unique offer name.                                                                                 |
| `status`              | No       | enum            | `draft` (default), `active`, `paused`, `archived`.                                                 |
| `categoryId`          | No       | string \| null  | ID of the parent category.                                                                         |
| `subCategoryId`       | No       | string \| null  | ID of the parent sub-category.                                                                     |
| `category`            | No       | string          | Legacy category label. Default `"engagement"`.                                                     |
| `group`               | No       | string          | Legacy group label. Default `""`.                                                                  |
| `priority`            | No       | integer (0-100) | Offer priority. Default `50`.                                                                      |
| `businessValue`       | No       | number (0-100)  | Business value score. Default `50`.                                                                |
| `mandatory`           | No       | boolean         | Mark as mandatory (admin-only). Default `false`.                                                   |
| `mandatoryReason`     | No       | string \| null  | Required when `mandatory` is `true`.                                                               |
| `mandatoryExpiresAt`  | No       | string \| null  | ISO 8601 expiry. Required when `mandatory` is `true`.                                              |
| `mandatoryApprovedBy` | No       | string \| null  | Approver ID. Required when `mandatory` is `true`.                                                  |
| `emergencyExcluded`   | No       | boolean         | Emergency exclusion flag. Default `false`.                                                         |
| `description`         | No       | string          | Full description.                                                                                  |
| `shortDesc`           | No       | string          | Short description for cards/lists.                                                                 |
| `eligibility`         | No       | object          | Eligibility rule config.                                                                           |
| `budget`              | No       | object          | Budget constraints.                                                                                |
| `schedule`            | No       | object          | Scheduling config.                                                                                 |
| `tags`                | No       | string\[]       | Free-form tags.                                                                                    |
| `productType`         | No       | string          | Product type label (e.g., `"credit_card"`, `"loan"`). Used by PRIE scoring for relevance matching. |
| `margin`              | No       | number          | Profit margin value. Feeds into the Impact (I) component of PRIE scoring.                          |
| `revenueValue`        | No       | number          | Expected revenue value. Feeds into the Impact (I) component of PRIE scoring.                       |
| `metadata`            | No       | object          | Arbitrary key-value metadata.                                                                      |

<Warning>
  Setting `mandatory` to `true` requires the `admin` role and all three governance fields: `mandatoryReason`, `mandatoryExpiresAt`, and `mandatoryApprovedBy`. Server-side validations enforce:

  * `mandatoryExpiresAt` must parse to a valid date and be **strictly in the future** (creates fail with `400` otherwise).
  * `mandatoryApprovedBy` must reference a real user in the same tenant.
  * That user must have role `admin` (rejecting attempts to set non-admin approvers).
</Warning>

### Example request

```json theme={null}
{
  "name": "Platinum Card Upgrade",
  "status": "draft",
  "categoryId": "cat_01",
  "priority": 75,
  "description": "Premium card upgrade for high-value customers.",
  "tags": ["credit-card", "premium"],
  "budget": { "maxPerDay": 500 }
}
```

### Response `201`

Returns the created offer object with all relations, `version: 1`, and `deletedAt: null`. An audit log entry is created with a `create` action.

### Error codes

| Code  | Reason                                                                                                                                                                                                   |
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Validation error (missing name, invalid fields, mandatory fields missing/invalid, a `categoryId`/`subCategoryId` that doesn't exist in your tenant, or a playground entity-quota that has been reached). |
| `401` | Missing or invalid API key / session.                                                                                                                                                                    |
| `403` | Non-admin trying to create mandatory offer.                                                                                                                                                              |
| `409` | An offer with that name already exists.                                                                                                                                                                  |
| `413` | Request body exceeds the 2 MB limit.                                                                                                                                                                     |
| `415` | `Content-Type` is not `application/json`.                                                                                                                                                                |

***

## Update an offer

```
PUT /api/v1/offers
```

Updates an existing offer. Only provided fields are changed. Triggers auto-assembly on status transitions. The `version` field is auto-incremented and a before/after audit snapshot is recorded.

### Request body

All fields from the create schema are accepted as optional, plus:

| Field | Required | Type   | Description             |
| ----- | -------- | ------ | ----------------------- |
| `id`  | **Yes**  | string | The offer ID to update. |

<Note>
  **Updates enforce the same guards as creates.** Turning an offer `mandatory` via `PUT` runs the identical governance validation as `POST` — `mandatoryReason`, `mandatoryExpiresAt` (must parse and be strictly in the future), and `mandatoryApprovedBy` (must be a real `admin` in the same tenant). You cannot slip a mandatory-override through the update path. Likewise, `categoryId` / `subCategoryId` are validated to belong to your tenant — an unknown or cross-tenant id is rejected with `400`, not silently linked.
</Note>

### Example request

```json theme={null}
{
  "id": "off_abc123",
  "status": "active",
  "priority": 90
}
```

### Response `200`

Returns the updated offer object with the incremented `version`.

### Error codes

| Code  | Reason                                                                                                                                                                               |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400` | Validation error, mandatory-override fields missing/invalid (non-future expiry, non-admin or foreign approver), or a `categoryId`/`subCategoryId` that doesn't exist in your tenant. |
| `401` | Missing or invalid API key / session.                                                                                                                                                |
| `403` | Non-admin trying to set mandatory.                                                                                                                                                   |
| `404` | No offer with that `id` (or name) in your tenant.                                                                                                                                    |
| `409` | Name conflict.                                                                                                                                                                       |
| `413` | Request body exceeds the 2 MB limit.                                                                                                                                                 |
| `415` | `Content-Type` is not `application/json`.                                                                                                                                            |

***

## Delete an offer (soft-delete)

```
DELETE /api/v1/offers?id={offerId}
```

Soft-deletes an offer by setting its `deletedAt` timestamp. **Cascade behavior:** all associated creatives are also soft-deleted. The `version` is incremented on the offer and each cascaded creative. An audit log entry is recorded for every affected entity.

### Query parameters

| Parameter | Required | Type   | Description         |
| --------- | -------- | ------ | ------------------- |
| `id`      | **Yes**  | string | Offer ID to delete. |

### Response `200`

```json theme={null}
{
  "success": true,
  "id": "off_abc123",
  "cascaded": 3
}
```

The `cascaded` field indicates how many child creatives were also soft-deleted.

### Error codes

| Code  | Reason                                                         |
| ----- | -------------------------------------------------------------- |
| `400` | Missing `id` query parameter, or the offer is already deleted. |
| `401` | Missing or invalid API key / session.                          |
| `403` | Insufficient role.                                             |
| `404` | No offer with that `id` (or name) in your tenant.              |

<Note>
  To restore a soft-deleted offer, use `POST /api/v1/restore?entityType=offer&id={offerId}` (admin only). Restoring an offer does not automatically restore cascaded creatives -- you must restore them individually.
</Note>

***

## Single-offer path endpoints

In addition to the collection routes above, each offer is addressable directly by id (or name) at `/api/v1/offers/{id}`. These path endpoints behave differently from the collection routes in a few important ways, noted below.

```
GET    /api/v1/offers/{id}
PUT    /api/v1/offers/{id}
DELETE /api/v1/offers/{id}
```

<Note>
  There is **no** `POST` or `PATCH` on this path. The `{id}` segment accepts either the offer UUID or its name.
</Note>

### `GET /api/v1/offers/{id}`

Returns the bare offer object. Unlike the list endpoint, the response does **not** include the `creatives`, `categoryRef`, or `subCategoryRef` relations. Returns `404` if no matching, non-deleted offer exists in your tenant. Minimum role `viewer`.

### `PUT /api/v1/offers/{id}`

Updates a single offer. Only these columns are accepted (any other field in the body is ignored): `name`, `description`, `shortDesc`, `priority`, `status`, `categoryId`, `subCategoryId`, `category`, `group`, `productType`, `businessValue`, `margin`, `revenueValue`, `emergencyExcluded`, `eligibility`, `budget`, `schedule`, `tags`, `metadata`.

<Warning>
  This path **cannot** set an offer mandatory — the `mandatory`, `mandatoryReason`, `mandatoryExpiresAt`, and `mandatoryApprovedBy` fields are not in the accepted set and are silently ignored. Use the collection `PUT /api/v1/offers` (with `id` in the body) to run the mandatory-override governance path. This path also does **not** increment the `version` field.
</Warning>

`categoryId` / `subCategoryId` are validated to belong to your tenant (`400` otherwise). Returns the updated offer object. Minimum role `editor`.

| Code  | Reason                                                                             |
| ----- | ---------------------------------------------------------------------------------- |
| `400` | Invalid JSON, or a `categoryId`/`subCategoryId` that doesn't exist in your tenant. |
| `401` | Missing or invalid API key / session.                                              |
| `403` | Insufficient role.                                                                 |
| `404` | No offer with that `id` (or name) in your tenant.                                  |
| `409` | An offer with that name already exists.                                            |
| `413` | Request body exceeds the 2 MB limit.                                               |

<Note>
  Unlike the collection routes, this path does not enforce a JSON `Content-Type` header, so it returns `400 Invalid JSON` (not `415`) for a malformed or non-JSON body.
</Note>

### `DELETE /api/v1/offers/{id}`

Soft-deletes a single offer by setting its `deletedAt` timestamp. Unlike the collection `DELETE`, this path does **not** cascade to child creatives and does **not** increment the `version` field.

```json theme={null}
{
  "deleted": true,
  "id": "off_abc123"
}
```

Returns `404` if no matching, non-deleted offer exists. Minimum role `editor`.

***

## Role requirements

| Method | Minimum role |
| ------ | ------------ |
| GET    | `viewer`     |
| POST   | `editor`     |
| PUT    | `editor`     |
| DELETE | `editor`     |

<Card title="Offers" icon="bullseye" href="/studio/offers">
  Learn more about creating and managing offers in the platform UI.
</Card>
