Skip to main content

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 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.
See the Contact Policies feature page for UI guidance and conceptual overview.

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

ParameterRequiredTypeDescription
limitNointegerMaximum results per page. Default 25.
cursorNostringCursor for keyset pagination.

Response 200

{
  "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

FieldRequiredTypeDescription
nameYesstring (1-255)Unique policy name.
descriptionNostringPolicy description.
statusNoenumdraft, active (default), paused, archived.
scopeNoenumglobal, offer (default), creative, channel, category, subcategory.
scopeIdNostring | nullID of the scoped entity (required when scope is not global).
ruleTypeYesenumOne of the rule types below.
configNoobjectRule-type-specific configuration.
priorityNointeger (0-100)Evaluation priority (higher = evaluated first). Default 50.
scopesNoarrayArray 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

Twelve rule types are supported. Each has a specific config object shape:
TypeDescriptionConfig shape
frequency_capLimit contacts within rolling time windows. Optional campaign filters scope counts to or away from specific batch campaign runs.{ maxPerDay?, maxPerWeek?, maxPerMonth?, maxTotal?, withinCampaignId?: string, excludeCampaignIds?: string[] }
cooldownMinimum hours between contacts.{ cooldownHours: number }
budget_exhaustedSuppress when impression or spend budget is consumed.{ budgetField: "impressions" | "spend", threshold: number }
outcome_basedSuppress after a specific outcome — or any outcome from a configured set.{ afterOutcome: string | string[], suppressForDays: number }
segment_exclusionExclude customers in named segments.{ excludeSegments: string[] }
time_windowRestrict to specific hours/days.{ startHour?, endHour?, daysOfWeek?, timezone? }
mutual_exclusionPrevent conflicting offers from being served together.{ excludeOfferIds: string[] }
cross_channel_capFrequency 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_capCustomer 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_capCap 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_overrideAllowlist that bypasses other blocking policies.{ reason?: string }
category_suppressionSuppress all offers in a category after any was shown.{ categoryId?, subCategoryId?, suppressionDays: number }
Campaign-aware frequency_cap: withinCampaignId (when set) restricts the cap to summary rows whose campaignId matches that Run.id — other campaigns and non-campaign sends are ignored. excludeCampaignIds drops impressions from the listed campaigns; rows with a null campaignId are still counted. InteractionHistory and InteractionSummary carry an additive nullable campaignId column populated by the writer when one is supplied.
offer_category_cap graceful degradation: InteractionSummary does not yet denormalize Offer.category. Until that schema enrichment ships, the engine conservatively counts all impressions in the period rather than filtering to the target category, so the cap fires earlier than a fully filtered count would.
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.
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.

Example request

{
  "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 twelve 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 and config.excludeCampaignIds (when set) must be a non-empty string array.
  • 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

CodeReason
400Validation error (missing name or ruleType, invalid timezone, invalid scope/priority).
409A contact policy with that name already exists.
415Content-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:
FieldRequiredTypeDescription
idYesstringThe policy ID to update.

Example request

{
  "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

ParameterRequiredTypeDescription
idYesstringPolicy ID to delete.

Response 200

{
  "deleted": true,
  "warnings": [
    "Referenced by decision flow \"Credit Card NBA\" — it will skip this policy."
  ]
}
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.
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.

Error codes

CodeReason
400Missing id query parameter, or soft-delete failed.

Role requirements

MethodMinimum role
GETviewer
POSTadmin
PUTadmin
DELETEadmin

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.
Updates also create audit snapshots via auditedUpdate, incrementing the rowVersion on each change. To include soft-deleted policies in GET responses, add ?includeDeleted=true to the query string.
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.

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

FieldRequiredTypeDescription
policyTypeYesstringCurrently only frequency_cap is fully supported by the count semantics.
scopeNoenumglobal (default), offer, category, channel.
scopeIdNostring | nullID of the scoped entity (required when scope is not global).
config.maxFrequencyYesinteger (>= 1)The cap value to project against.
config.periodDaysYesinteger (1-365)Lookback window.
config.outcomeTypeYesstringInteraction type to count (typically impression).
sampleSizeNointeger (100-10000)Customer sample size. Default 1000.

Response 200

{
  "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).

Contact Policies

Learn more about configuring contact policies in the platform UI.