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

# Qualification Rules API

> Create, update, list, and delete qualification rules that filter offers during decisioning.

<Note>
  **See also**: [Qualification Rules concept and configuration](/decisioning/qualification-rules) for what this API powers, when to call it, and how it is configured.
</Note>

Qualification rules define conditions that offers must satisfy before they can be recommended to a customer. Rules are evaluated during the qualification stage of a Decision Flow and can be scoped globally or to a specific segment, channel, category, sub-category, offer, or placement.

<Info>
  See the [Qualification Rules feature page](/decisioning/qualification-rules) for UI guidance and conceptual overview.
</Info>

## Base path

```
/api/v1/qualification-rules
```

***

## List qualification rules

```
GET /api/v1/qualification-rules
```

Returns a paginated list of qualification rules, ordered by priority (highest first), then creation date (newest first).

### Query parameters

| Parameter        | Required | Type    | Description                                                                                                                                                                    |
| ---------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `limit`          | No       | integer | Maximum results per page. Default `50`, max `100`.                                                                                                                             |
| `cursor`         | No       | string  | Cursor for keyset pagination. Pass the last `id` from the previous page.                                                                                                       |
| `includeDeleted` | No       | boolean | Include soft-deleted records. Default `false`.                                                                                                                                 |
| `stage`          | No       | enum    | Filter by Decisioning Gates stage: `eligibility`, `fit`, `match`, `ranking`. Filters rows whose classified stage matches. Returns `400` if the value is not one of those four. |

Each row in the response is enriched with a `decisioningStage` field — the classified stage (one of `eligibility`, `fit`, `match`, `ranking`) — so the UI can render the three-section layout without re-running the classifier client-side. Legacy `stage` values (`qualification` → `eligibility`, `applicability` → `fit`, `suitability` → `match`) are mapped automatically.

### Response `200`

```json theme={null}
{
  "data": [
    {
      "id": "qr_001",
      "tenantId": "t_001",
      "name": "Gold Tier Required",
      "description": "Only show to gold tier or above.",
      "status": "active",
      "scope": "segment",
      "scopeId": "seg_gold",
      "scopes": [{ "id": "qs_001", "qualificationRuleId": "qr_001", "scope": "segment", "scopeId": "seg_gold", "createdAt": "2026-03-10T12:00:00.000Z" }],
      "ruleType": "segment_required",
      "config": { "segmentId": "seg_gold" },
      "priority": 80,
      "stage": "eligibility",
      "decisioningStage": "eligibility",
      "version": 1,
      "deletedAt": null,
      "createdAt": "2026-03-10T12:00:00.000Z",
      "updatedAt": "2026-03-12T09:30:00.000Z"
    }
  ],
  "pagination": {
    "total": 15,
    "limit": 25,
    "hasMore": false,
    "cursor": null
  }
}
```

***

## Create a qualification rule

```
POST /api/v1/qualification-rules
```

Creates a new qualification rule. Requires the `admin` role.

### Request body

| Field         | Required | Type            | Description                                                                                                                                                                                                                                                                                                                                                                                |
| ------------- | -------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`        | **Yes**  | string (1-255)  | Unique rule name.                                                                                                                                                                                                                                                                                                                                                                          |
| `description` | No       | string          | Rule description.                                                                                                                                                                                                                                                                                                                                                                          |
| `status`      | No       | enum            | `draft`, `active` (default), `paused`, `archived`.                                                                                                                                                                                                                                                                                                                                         |
| `scope`       | No       | enum            | `global` (default), `segment`, `channel`, `category`, `subcategory`, `offer`, `creative`, `placement`.                                                                                                                                                                                                                                                                                     |
| `scopeId`     | No       | string \| null  | ID of the scoped entity (required when scope is not `global`).                                                                                                                                                                                                                                                                                                                             |
| `ruleType`    | **Yes**  | enum            | One of: `segment_required`, `attribute_condition`, `offer_attribute`, `propensity_threshold`, `recency_check`, `metric_condition`. The `config` schema is validated as a discriminated union — see the per-rule-type schemas below.                                                                                                                                                        |
| `config`      | **Yes**  | object          | Rule-type-specific configuration. Schema depends on `ruleType` (see table below).                                                                                                                                                                                                                                                                                                          |
| `priority`    | No       | integer (0-100) | Evaluation priority (higher = evaluated first). Default `50`.                                                                                                                                                                                                                                                                                                                              |
| `stage`       | No       | enum            | Decisioning Gates stage: `eligibility` (default), `fit`, `match`, `ranking`. Legacy `qualification` is auto-mapped to `eligibility` on read.                                                                                                                                                                                                                                               |
| `scopes`      | No       | array           | Array of scope assignments. Each item: `{ scope: "global"\|"segment"\|"channel"\|"category"\|"subcategory"\|"offer"\|"creative"\|"placement", scopeId: "entity-UUID" \| null }`. When provided, overrides the legacy `scope`/`scopeId` fields and persists multiple assignments via the `qualification_rule_scopes` join table. A rule can have multiple scope assignments simultaneously. |

### Rule types and config schemas

Each `ruleType` validates against a typed `config` schema at the API boundary — misspelled or missing required fields are rejected with `400`.

| Type                   | Description                                                                                                                                                                                                | Config schema                                                                                                                                                                                                                                                                                                                         |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `segment_required`     | Customer must be in the named segment.                                                                                                                                                                     | `{ "segmentId": "<segment-id>" }` — `segmentId` required (string, non-empty).                                                                                                                                                                                                                                                         |
| `attribute_condition`  | **Customer** attribute must match a condition.                                                                                                                                                             | `{ "attribute": "<path>", "operator": "<op>", "value": <any> }` — `operator` ∈ `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`, `not_in`, `contains`, `not_contains`, `exists`, `not_exists`. `value` may be omitted for `exists`/`not_exists`, and **must be an array for `in`/`not_in`** (a scalar is rejected `400` — see note below). |
| `offer_attribute`      | **Offer** field must match a condition (e.g. `productType`, `margin`, `revenueValue`, or a custom field). Same shape and operator set as `attribute_condition`, but read from the offer, not the customer. | `{ "attribute": "<offer-field>", "operator": "<op>", "value": <any> }` — `operator` ∈ `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`, `not_in`, `contains`, `not_contains`, `exists`, `not_exists`. `value` may be omitted for `exists`/`not_exists`, and **must be an array for `in`/`not_in`**.                                        |
| `propensity_threshold` | Model score must clear a threshold (model-based), or an enriched attribute must satisfy a comparison (attribute-based).                                                                                    | Model-based: `{ "propensityModel": "<name>", "minScore": <number> }`. Attribute-based: `{ "attribute": "<path>", "operator": "<op>", "threshold": <number> }` — `operator` ∈ `gte`, `gt`, `lte`, `lt`. All fields optional (mode is selected by which are present); legacy `{ "threshold", "modelId" }` is still accepted.            |
| `recency_check`        | Days-since-last-activity attribute must be within a window.                                                                                                                                                | `{ "attribute": "<path>", "withinDays": <positive-int> }` — both required.                                                                                                                                                                                                                                                            |
| `metric_condition`     | A behavioral metric must meet a numeric condition.                                                                                                                                                         | `{ "metricId": "<metric-id>", "operator": "<op>", "threshold": <number>, "dimensionMapping"?: { "<dim>": "$candidate.offerId" } }` — `operator` ∈ `eq`, `lt`, `lte`, `gt`, `gte`. Legacy `value` is accepted and normalized to `threshold`.                                                                                           |

Every rule type's config additionally accepts an optional **`onMissing`** field (`"skip"` | `"block"`, default `"skip"`) controlling the missing-data path: `"skip"` fail-opens (rule skipped, warning recorded in the decision trace); `"block"` fail-closes (candidate rejected with `Required attribute "<attr>" not present`). See [Missing data: `onMissing`](/decisioning/qualification-rules#missing-data-onmissing).

### Example request

```json theme={null}
{
  "name": "Minimum Age 18",
  "ruleType": "attribute_condition",
  "scope": "global",
  "config": {
    "attribute": "customer.age",
    "operator": "gte",
    "value": 18
  },
  "priority": 90,
  "stage": "eligibility"
}
```

### Response `201`

Returns the created qualification rule object.

### Validation

All fields are validated via Zod schemas (`CreateQualificationRuleSchema` is a discriminated union on `ruleType`):

* `name`: 1-255 characters, must be unique per tenant.
* `ruleType`: Must be one of the six enum values; `config` is then validated against the typed schema for that variant. `attribute_condition` (reads a customer attribute) and `offer_attribute` (reads an offer field) share an identical operator set, including `not_in`, `contains`, and `not_contains` (the negation of `contains` — true when a list attribute does **not** include the value, e.g. `customer.opt_outs not_contains "all"`).
* **`in` / `not_in` require an array `value`.** These operators evaluate array membership, so a scalar (`"value": "gold"` instead of `["gold"]`) silently turns the gate into a no-op — `not_in` would then pass *every* candidate and `in` would block *every* candidate, the opposite of the author's intent. The API rejects a scalar `value` for these two operators with `400` and a message naming the failure. Use `["gold","platinum"]`.
* `scope`: Must be one of the eight enum values (`global`, `segment`, `channel`, `category`, `subcategory`, `offer`, `creative`, `placement`).
* `priority`: Integer, 0-100.
* `stage`: Must be `eligibility`, `fit`, `match`, or `ranking`.

### Error codes

| Code  | Reason                                                                                                                                     |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `400` | Validation error (missing name or ruleType, invalid scope/priority, or a `config` that doesn't match the typed schema for the `ruleType`). |
| `401` | Missing or invalid API key / session.                                                                                                      |
| `403` | Insufficient role (write endpoints require `admin`).                                                                                       |
| `409` | A qualification rule with that name already exists.                                                                                        |
| `413` | Request body exceeds the 2 MB limit.                                                                                                       |
| `415` | `Content-Type` is not `application/json`.                                                                                                  |

***

## Update a qualification rule

```
PUT /api/v1/qualification-rules
```

Updates an existing qualification rule. Only provided fields are changed. Requires the `admin` role.

### Request body

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

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

### Example request

```json theme={null}
{
  "id": "qr_001",
  "priority": 95,
  "status": "paused"
}
```

### Response `200`

Returns the updated qualification rule object.

***

## Delete a qualification rule

```
DELETE /api/v1/qualification-rules?id={ruleId}
```

Soft-deletes a qualification rule by ID. The record is marked as deleted but retained in the database for audit purposes. The response includes warnings if the rule is still referenced by any Decision Flow's `draftConfig`.

### Query parameters

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

### Response `200`

```json theme={null}
{
  "deleted": true,
  "warnings": [
    "Referenced by decision flow \"Credit Card NBA\" — it will skip this rule."
  ]
}
```

<Note>
  This endpoint uses **soft-delete** -- the record is not physically removed from the database. It is excluded from GET results by default. To include soft-deleted records, pass `?includeDeleted=true` on the GET request.
</Note>

<Note>
  Unlike most DELETE endpoints that return `204`, this endpoint returns `200` with a body so it can communicate ghost-reference warnings. The API checks all Decision Flows in the tenant for references to this rule ID in their `draftConfig.stages.filter.qualificationRuleIds` array.
</Note>

### Error codes

| Code  | Reason                                                         |
| ----- | -------------------------------------------------------------- |
| `400` | Missing `id` query parameter, or soft-delete failed.           |
| `401` | Missing or invalid API key / session.                          |
| `403` | Insufficient role (requires `admin`).                          |
| `404` | No qualification rule with that `id` (or name) in your tenant. |

***

## Role requirements

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

***

## Soft-delete and audit

Qualification rules use **soft-delete** with audit snapshots. When a rule is deleted:

1. The `deletedAt` timestamp is set (record is retained).
2. An audit snapshot is captured with the full state before deletion.
3. Ghost reference warnings are returned if the rule is still referenced by any Decision Flow.

Updates also create audit snapshots via `auditedUpdate`, incrementing the `version` field on each change. This provides a full change history for compliance and debugging.

To include soft-deleted rules in GET responses, add `?includeDeleted=true` to the query string.

<Note>
  Multi-scope rules are evaluated differently: global-scoped rules are evaluated in the Decision Flow's Qualify node, while entity-scoped rules (offer, category, channel, creative) are automatically evaluated per-candidate during the recommend pipeline.
</Note>

<Card title="Decisioning Gates" icon="filter" href="/decisioning/qualification-rules">
  Learn more about configuring decisioning gates in the platform UI.
</Card>
