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

# Contact Policies API

> Create, update, list, and delete contact policies that govern communication frequency and suppression.

<Note>
  **See also**: [Contact Policies concept and configuration](/decisioning/contact-policies) for what this API powers, when to call it, and how it is configured.
</Note>

Contact policies control how often and under what conditions a customer can be contacted. They enforce frequency caps, cooldowns, budget exhaustion, mutual exclusion, and other suppression rules during the filtering stage of a Decision Flow.

<Info>
  See the [Contact Policies feature page](/decisioning/contact-policies) for UI guidance and conceptual overview.
</Info>

## Base path

```
/api/v1/contact-policies
```

***

## List contact policies

```
GET /api/v1/contact-policies
```

Returns a paginated list of contact policies, 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       | string  | Set to `"true"` to include soft-deleted policies in the results.         |

### Response `200`

```json theme={null}
{
  "data": [
    {
      "id": "cp_001",
      "tenantId": "t_001",
      "name": "Email Weekly Cap",
      "description": "No more than 3 emails per week per customer.",
      "status": "active",
      "scope": "channel",
      "scopeId": "ch_email",
      "scopes": [{ "id": "cps_001", "contactPolicyId": "cp_001", "scope": "channel", "scopeId": "ch_email", "createdAt": "2026-03-10T12:00:00.000Z" }],
      "ruleType": "frequency_cap",
      "config": {
        "maxPerDay": 1,
        "maxPerWeek": 3
      },
      "priority": 80,
      "version": 1,
      "deletedAt": null,
      "createdAt": "2026-03-10T12:00:00.000Z",
      "updatedAt": "2026-03-12T09:30:00.000Z"
    }
  ],
  "pagination": {
    "total": 8,
    "limit": 25,
    "hasMore": false,
    "cursor": null
  }
}
```

***

## Create a contact policy

```
POST /api/v1/contact-policies
```

Creates a new contact policy. Requires the `admin` role.

### Request body

| Field         | Required | Type            | Description                                                                                                                                                                                                                                                                 |
| ------------- | -------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | **Yes**  | string (1-255)  | Unique policy name.                                                                                                                                                                                                                                                         |
| `description` | No       | string          | Policy description.                                                                                                                                                                                                                                                         |
| `status`      | No       | enum            | `draft`, `active` (default), `paused`, `archived`.                                                                                                                                                                                                                          |
| `scope`       | No       | enum            | `global`, `offer` (default), `creative`, `channel`, `category`, `subcategory`.                                                                                                                                                                                              |
| `scopeId`     | No       | string \| null  | ID of the scoped entity (required when scope is not `global`).                                                                                                                                                                                                              |
| `ruleType`    | **Yes**  | enum            | One of the rule types below.                                                                                                                                                                                                                                                |
| `config`      | No       | object          | Rule-type-specific configuration.                                                                                                                                                                                                                                           |
| `priority`    | No       | integer (0-100) | Evaluation priority (higher = evaluated first). Default `50`.                                                                                                                                                                                                               |
| `scopes`      | No       | array           | Array of scope assignments. Each item: `{ scope: "global"\|"offer"\|"creative"\|"channel"\|"category"\|"subcategory", scopeId: "entity-UUID" }`. When provided, overrides the legacy `scope`/`scopeId` fields. A policy can have multiple scope assignments simultaneously. |

### Rule types and config shapes

Fourteen rule types are supported. Each has a specific `config` object shape:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Config shape                                                                                                                                                                                                                                                                                                                                                  |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `frequency_cap`        | Limit contacts within fixed calendar windows. Optional campaign filters scope counts to or away from specific batch campaign runs; `campaignScoped: true` makes it a per-campaign-run cap that is skipped on real-time traffic.                                                                                                                                                                                                                                                                              | `{ maxPerDay?, maxPerWeek?, maxPerMonth?, maxTotal?, withinCampaignId?: string, excludeCampaignIds?: string[], campaignScoped?: boolean }`                                                                                                                                                                                                                    |
| `cooldown`             | Minimum hours between contacts.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{ cooldownHours: number }`                                                                                                                                                                                                                                                                                                                                   |
| `budget_exhausted`     | Suppress when impression or spend budget is consumed.                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `{ budgetField: "impressions" \| "spend", threshold: number }`                                                                                                                                                                                                                                                                                                |
| `outcome_based`        | Suppress after a specific outcome — or any outcome from a configured set.                                                                                                                                                                                                                                                                                                                                                                                                                                    | `{ afterOutcome: string \| string[], suppressForDays: number }`                                                                                                                                                                                                                                                                                               |
| `segment_exclusion`    | Exclude customers in named segments.                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `{ excludeSegments: string[] }`                                                                                                                                                                                                                                                                                                                               |
| `time_window`          | Restrict to specific hours/days.                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{ startHour?, endHour?, daysOfWeek?, timezone? }`                                                                                                                                                                                                                                                                                                            |
| `mutual_exclusion`     | Prevent conflicting offers from being served together.                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `{ excludeOfferIds: string[] }`                                                                                                                                                                                                                                                                                                                               |
| `cross_channel_cap`    | Frequency cap aggregated across all channels for one Offer. Optional `appliesAcross` narrows the count to a specific channel-id list (omit / empty = all channels, legacy behaviour).                                                                                                                                                                                                                                                                                                                        | `{ maxTotal: number, periodType?: "daily" \| "weekly" \| "monthly", appliesAcross?: string[] }`                                                                                                                                                                                                                                                               |
| `customer_total_cap`   | Customer Communication Budget — total contacts a single customer receives across **all** Offers, channels, and creatives in a window.                                                                                                                                                                                                                                                                                                                                                                        | `{ maxTotal: number, periodType?: "daily" \| "weekly" \| "monthly" \| "alltime" }`                                                                                                                                                                                                                                                                            |
| `offer_category_cap`   | Cap total contacts a customer receives in a specific `Offer.category` (free-form marketing string) per period.                                                                                                                                                                                                                                                                                                                                                                                               | `{ targetCategory: string, maxTotal: number, periodType?: "daily" \| "weekly" \| "monthly" \| "alltime" }`                                                                                                                                                                                                                                                    |
| `allow_override`       | Allowlist that bypasses other blocking policies for a **matching** candidate (evaluated before all blocking rules). `allowMandatory: true` alone un-blocks **mandatory Offers only**; `allowSegments`/`allowOfferIds` gate by customer segment / Offer id (both must hold when both are set); an empty `{}` config is a **blanket** override that matches every candidate.                                                                                                                                   | `{ allowMandatory?: boolean, allowSegments?: string[], allowOfferIds?: string[] }`                                                                                                                                                                                                                                                                            |
| `category_suppression` | Suppress all offers in a category after any was shown.                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `{ categoryId?, subCategoryId?, suppressionDays: number }`                                                                                                                                                                                                                                                                                                    |
| `do_not_contact`       | Globally suppresses every candidate this policy is scoped to. **The only mechanism that suppresses across channels** — once attached to a customer (typically by an upstream DNC qualification step) the policy blocks every recommend candidate in scope. Usually paired with `scope: "global"`.                                                                                                                                                                                                            | `{ dncSource?: "internal_dnc" \| string, reason?: string }` (config is informational; the engine honours the policy purely on its scope)                                                                                                                                                                                                                      |
| `metric_condition`     | **Behavioral-metric-driven suppression.** Reads the per-customer value of a Behavioral Metric (computed by aggregating `InteractionHistory` / `InteractionSummary` rollups — see [Behavioral Metrics API](/api-reference/behavioral-metrics)) and blocks the candidate when the operator/threshold trips. This is the bridge between the metrics module and the contact-policy engine — use it to express things like "suppress when 30-day impression count > 12" or "suppress when complaint rate ≥ 0.05". | `{ metricId: string, operator?: "gt" \| "gte" \| "lt" \| "lte" \| "eq", threshold?: number }` — when the per-customer metric value evaluates `value <op> threshold` true, the candidate is blocked. Defaults: `operator = "gte"`, `threshold = 0`. Requires that the recommend pipeline has loaded MetricValue rows for the customer (handled automatically). |

<Note>
  **Campaign-aware `frequency_cap`**: `withinCampaignId` (when set) restricts the cap to summary rows whose `campaignId` matches that batch-run id — other campaigns and non-campaign sends are ignored. `excludeCampaignIds` drops impressions from the listed campaigns; rows with no campaign attached are still counted. `campaignScoped: true` marks the cap as **per-campaign-run**: it only applies while a batch campaign run is evaluating candidates and counts only impressions recorded within that run — real-time `/recommend` traffic skips the rule entirely (an explicit `withinCampaignId` still takes precedence over the current run's id). Interaction history and summaries carry an additive nullable `campaignId` field populated when one is supplied at send time.
</Note>

<Note>
  **`offer_category_cap` matching**: `targetCategory` is matched case-insensitively against the candidate's `Offer.category` marketing string (with a fallback match on the taxonomy `categoryId`). The interaction-summary store denormalizes `Offer.category` onto each row, so the cap counts only contacts in the target category. Legacy summary rows written before that denormalization carry a `NULL` category and are not counted.
</Note>

<Warning>
  For `time_window` rules, the `timezone` value in `config` is validated against the IANA timezone database at write time. Invalid timezones are rejected with a `400` error.
</Warning>

<Note>
  For `outcome_based` rules, `config.afterOutcome` accepts either a non-empty string (legacy single-outcome form) or a non-empty array of strings (preferred). Empty values are rejected with `outcome_based requires afterOutcome as a non-empty string or non-empty string array`. `config.suppressForDays` must be `>= 0`. The Studio UI exposes a one-click **Adverse Outcomes preset** that fills the array with `["complaint", "unsubscribe", "hard_bounce", "spam_report"]` for the standard 90-day compliance quarantine.
</Note>

### Example request

```json theme={null}
{
  "name": "Email Weekly Cap",
  "description": "No more than 3 emails per week per customer.",
  "scope": "channel",
  "scopeId": "ch_email",
  "ruleType": "frequency_cap",
  "config": {
    "maxPerDay": 1,
    "maxPerWeek": 3
  },
  "priority": 80
}
```

### Response `201`

Returns the created contact policy object.

### Validation

All fields are validated via Zod schemas:

* `name`: 1-255 characters, must be unique per tenant.
* `ruleType`: Must be one of the fourteen enum values listed above. For `customer_total_cap`, `config.maxTotal` is required (non-negative number) and `config.periodType` (if set) must be one of `daily`, `weekly`, `monthly`, `alltime`. For `offer_category_cap`, `config.targetCategory` (non-empty string) and `config.maxTotal` (non-negative number) are required, and `config.periodType` follows the same enum. For `frequency_cap`, `config.withinCampaignId` (when set) must be a non-empty string, `config.excludeCampaignIds` (when set) must be a non-empty string array, and `config.campaignScoped` (when set) must be a boolean.
* `scope`: Must be one of: `global`, `offer`, `creative`, `channel`, `category`, `subcategory`.
* `priority`: Integer, 0-100.
* `config`: Free-form JSON object (structural validation per rule type is not enforced by the schema).

### Error codes

| Code  | Reason                                                                                                                                         |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Validation error (missing name or ruleType, invalid timezone, invalid scope/priority, or a `config` that fails the per-`ruleType` refinement). |
| `401` | Missing or invalid API key / session.                                                                                                          |
| `403` | Insufficient role (write endpoints require `admin`).                                                                                           |
| `409` | A contact policy with that name already exists.                                                                                                |
| `413` | Request body exceeds the 2 MB limit.                                                                                                           |
| `415` | `Content-Type` is not `application/json`.                                                                                                      |

***

## Update a contact policy

```
PUT /api/v1/contact-policies
```

Updates an existing contact policy. 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 policy ID to update. |

### Example request

```json theme={null}
{
  "id": "cp_001",
  "config": {
    "maxPerDay": 2,
    "maxPerWeek": 5
  }
}
```

### Response `200`

Returns the updated contact policy object.

***

## Delete a contact policy

```
DELETE /api/v1/contact-policies?id={policyId}
```

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

### Query parameters

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

### Response `200`

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

<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>
  The API checks all Decision Flows in the tenant for references to this policy ID in their `draftConfig.stages.filter.contactPolicyIds` array and returns warnings for any matches.
</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 contact policy 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

Contact policies use **soft-delete** with audit snapshots. When a policy 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 policy is still referenced by any Decision Flow.
4. Any **materialized suppressions** written by this policy (see [Suppressions](/api-reference/suppressions)) are removed, and the per-tenant policy cache is invalidated — the policy stops firing on the very next `/recommend`, not after a cache TTL. Deactivating a policy (`PUT` with `status` other than `active`) clears its suppressions the same way. As a backstop, decision-time suppression reads also ignore any row whose policy is no longer active.

Creates and updates invalidate the same cache, so a new or edited policy is enforced on the next `/recommend` immediately.

Updates also create audit snapshots via `auditedUpdate`, incrementing the `version` field on each change.

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

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

***

## Preview policy impact

```
POST /api/v1/contact-policies/impact-preview
```

Projects how a proposed `frequency_cap`-style policy would have affected
recent traffic. Samples up to 1,000 active customers, counts matching
interactions in the lookback window, and returns affected counts plus an
optional segment breakdown.

### Request body

| Field                 | Required | Type                | Description                                                               |
| --------------------- | -------- | ------------------- | ------------------------------------------------------------------------- |
| `policyType`          | **Yes**  | string              | Currently only `frequency_cap` is fully supported by the count semantics. |
| `scope`               | No       | enum                | `global` (default), `offer`, `category`, `channel`.                       |
| `scopeId`             | No       | string \| null      | ID of the scoped entity (required when scope is not `global`).            |
| `config.maxFrequency` | **Yes**  | integer (>= 1)      | The cap value to project against.                                         |
| `config.periodDays`   | **Yes**  | integer (1-365)     | Lookback window.                                                          |
| `config.outcomeType`  | **Yes**  | string              | Interaction type to count (typically `impression`).                       |
| `sampleSize`          | No       | integer (100-10000) | Customer sample size. Default `1000`.                                     |

### Response `200`

```json theme={null}
{
  "totalCustomersAnalyzed": 842,
  "customersAffected": 191,
  "affectedPercent": 22.7,
  "offersSuppressed": { "Welcome Offer": 91, "Mortgage Refi": 14 },
  "avgOffersBeforePolicy": 4.7,
  "avgOffersAfterPolicy": 3.4,
  "topAffectedSegments": [
    { "segment": "active_email_subscribers", "affected": 110, "total": 240 }
  ]
}
```

The Studio Contact Policies editor surfaces this endpoint as a **Preview Impact**
button on the `frequency_cap` config panel. The button derives `maxFrequency`
and `periodDays` from the largest cap on the form (daily → 1 day, weekly → 7,
monthly → 30, total → 365).

<Card title="Contact Policies" icon="shield-halved" href="/decisioning/contact-policies">
  Learn more about configuring contact policies in the platform UI.
</Card>
