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.
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.
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).
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) 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).
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. Interaction history and summaries carry an additive nullable campaignId field populated when one is supplied at send time.
offer_category_cap graceful degradation: the interaction-summary store does not yet denormalize the 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.
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 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).
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.
{ "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.
Contact policies use soft-delete with audit snapshots. When a policy is deleted:
The deletedAt timestamp is set (record is retained).
An audit snapshot is captured with the full state before deletion.
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.
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.
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.