Segmenting NICE CXone Outbound Contact Lists Dynamically with Python
What You Will Build
- A Python script that extracts historical interaction data, calculates recency and frequency scores, creates segmented contact lists, and launches predictive campaigns with dynamic abandonment thresholds.
- This implementation uses the NICE CXone REST APIs for Interactions, Lists, and Campaigns.
- The tutorial covers Python 3.9+ with the
requestslibrary and standard type hints.
Prerequisites
- OAuth 2.0 Client Credentials grant configured in CXone with the following scopes:
interactions:read,lists:read,lists:write,campaigns:read,campaigns:write - CXone API version 1 (v1)
- Python 3.9 or higher
- External dependencies:
requests>=2.28.0,urllib3>=1.26.0 - Valid CXone tenant URL (e.g.,
https://api.mynicecx.comor tenant-specific endpoint)
Authentication Setup
CXone uses standard OAuth 2.0 Client Credentials flow. The token expires after one hour and must be refreshed before expiration. The code below implements a session-based client with automatic retry logic for rate limits and token caching.
import time
import logging
import requests
from typing import Dict, Optional
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
class CXoneClient:
def __init__(self, tenant_url: str, client_id: str, client_secret: str):
self.tenant_url = tenant_url.rstrip("/")
self.client_id = client_id
self.client_secret = client_secret
self.token: Optional[str] = None
self.token_expiry: float = 0.0
self.session = self._build_session()
def _build_session(self) -> requests.Session:
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1.5,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "PATCH"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
def _get_token(self) -> str:
if self.token and time.time() < self.token_expiry - 300:
return self.token
url = f"{self.tenant_url}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "interactions:read lists:read lists:write campaigns:read campaigns:write"
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = self.session.post(url, data=payload, headers=headers)
response.raise_for_status()
data = response.json()
self.token = data["access_token"]
self.token_expiry = time.time() + data["expires_in"]
return self.token
def _make_request(self, method: str, path: str, **kwargs) -> requests.Response:
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {self._get_token()}"
headers["Content-Type"] = "application/json"
url = f"{self.tenant_url}{path}"
return self.session.request(method, url, headers=headers, **kwargs)
The _build_session method attaches an HTTPAdapter with a Retry strategy. This automatically handles 429 Too Many Requests responses with exponential backoff, which prevents cascade failures during bulk list operations. The _get_token method caches the token and refreshes it only when expiration approaches within a five-minute window.
Implementation
Step 1: Query Interaction API for Historical Engagement Metrics
The CXone Interaction API requires a POST request to /api/v1/interactions/search. The request body accepts filter criteria and pagination tokens. You must specify the time window and interaction types to retrieve relevant outbound history.
from datetime import datetime, timedelta
from typing import List, Dict, Any
def fetch_interactions(client: CXoneClient, contact_ids: List[str], days_back: int = 90) -> List[Dict[str, Any]]:
"""
Retrieves historical interactions for a set of contacts.
Required scope: interactions:read
"""
start_date = (datetime.utcnow() - timedelta(days=days_back)).isoformat() + "Z"
end_date = datetime.utcnow().isoformat() + "Z"
all_interactions: List[Dict[str, Any]] = []
next_token: Optional[str] = None
while True:
payload = {
"filters": {
"contactIds": contact_ids,
"startTime": start_date,
"endTime": end_date,
"interactionType": ["voice"]
},
"pageSize": 500,
"pageToken": next_token
}
response = client._make_request("POST", "/api/v1/interactions/search", json=payload)
if response.status_code == 429:
logger.warning("Rate limited on interaction query. Retrying...")
time.sleep(2)
continue
response.raise_for_status()
data = response.json()
results = data.get("data", [])
all_interactions.extend(results)
pagination = data.get("pagination", {})
next_token = pagination.get("nextPageToken")
if not next_token:
break
logger.info(f"Retrieved {len(all_interactions)} interactions for {len(contact_ids)} contacts.")
return all_interactions
The API returns a data array containing interaction objects and a pagination object with nextPageToken. The loop continues until nextPageToken is null. Each interaction object contains id, startTime, contactId, and type. The 500 record page size balances throughput with memory usage.
Step 2: Apply Scoring Algorithm Based on Recency and Frequency Attributes
You must transform raw interaction timestamps into actionable scores. The algorithm calculates recency as days since the last interaction and frequency as the total interaction count within the window. A weighted score determines segment priority.
from collections import defaultdict
def calculate_segment_scores(interactions: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
"""
Computes recency, frequency, and priority scores per contact.
Returns a dictionary keyed by contactId.
"""
contact_metrics: Dict[str, Dict[str, Any]] = defaultdict(lambda: {
"frequency": 0,
"last_interaction": None,
"score": 0.0,
"priority": "low"
})
now = datetime.utcnow()
for interaction in interactions:
contact_id = interaction.get("contactId")
if not contact_id:
continue
start_time_str = interaction.get("startTime")
if not start_time_str:
continue
start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00"))
days_since = (now - start_time).total_seconds() / 86400.0
metrics = contact_metrics[contact_id]
metrics["frequency"] += 1
if metrics["last_interaction"] is None or start_time > metrics["last_interaction"]:
metrics["last_interaction"] = start_time
metrics["days_since"] = days_since
for contact_id, metrics in contact_metrics.items():
# Recency score: 0-50 points (lower days_since = higher score)
recency_score = max(0, 50 - (metrics["days_since"] * 0.5))
# Frequency score: 0-50 points (capped at 20 interactions)
freq_score = min(metrics["frequency"], 20) * 2.5
metrics["score"] = recency_score + freq_score
if metrics["score"] >= 70:
metrics["priority"] = "high"
elif metrics["score"] >= 40:
metrics["priority"] = "medium"
else:
metrics["priority"] = "low"
return dict(contact_metrics)
The scoring formula weights recency and frequency equally at 50 points each. Recency decays linearly by 0.5 points per day. Frequency caps at 20 interactions to prevent outliers from dominating the score. The priority thresholds map directly to campaign abandonment thresholds in the next step.
Step 3: Generate Filtered Contact Subsets Using the List API
CXone requires explicit list creation before contact assignment. You will create three lists corresponding to the priority segments, then batch-add contacts to each list. The List API accepts up to 1,000 contacts per request, but 500 provides safer payload sizes.
def create_segment_lists(client: CXoneClient, prefix: str) -> Dict[str, str]:
"""
Creates three outbound lists for high, medium, and low priority segments.
Required scope: lists:write
"""
list_ids = {}
for priority in ["high", "medium", "low"]:
payload = {
"name": f"{prefix}_segment_{priority}",
"description": f"Dynamic segment for {priority} priority contacts",
"type": "outbound"
}
response = client._make_request("POST", "/api/v1/lists", json=payload)
response.raise_for_status()
list_ids[priority] = response.json()["id"]
logger.info(f"Created list {list_ids[priority]} for {priority} priority.")
return list_ids
def assign_contacts_to_lists(client: CXoneClient, list_ids: Dict[str, str], metrics: Dict[str, Dict[str, Any]]) -> None:
"""
Batches contacts into their respective priority lists.
Required scope: lists:write
"""
batches: Dict[str, List[Dict[str, Any]]] = {k: [] for k in list_ids}
for contact_id, data in metrics.items():
contact_payload = {
"contactId": contact_id,
"data": {
"priority": data["priority"],
"score": data["score"]
}
}
batches[data["priority"]].append(contact_payload)
for priority, contacts in batches.items():
list_id = list_ids[priority]
for i in range(0, len(contacts), 500):
batch = contacts[i:i+500]
response = client._make_request("POST", f"/api/v1/lists/{list_id}/contacts", json=batch)
if response.status_code == 429:
logger.warning("Rate limited on list assignment. Backing off...")
time.sleep(2)
continue
response.raise_for_status()
logger.info(f"Assigned {len(batch)} contacts to {priority} list.")
The contactId field must match the identifier used in the interaction history. The data object stores custom attributes that CXone preserves for reporting. Batch processing prevents payload size errors and distributes load across the API gateway.
Step 4: Trigger Predictive Dialing Campaigns with Adjusted Abandonment Thresholds
Campaign creation requires explicit dial type configuration and abandonment thresholds. You will map segment priority to threshold values: high priority receives 3%, medium receives 5%, and low receives 8%. The campaign payload must include start time, list ID, and wrap-up configuration.
def launch_predictive_campaigns(client: CXoneClient, list_ids: Dict[str, str], prefix: str) -> Dict[str, str]:
"""
Creates predictive campaigns with priority-based abandonment thresholds.
Required scope: campaigns:write
"""
threshold_map = {
"high": 3.0,
"medium": 5.0,
"low": 8.0
}
campaign_ids = {}
start_time = (datetime.utcnow() + timedelta(minutes=15)).isoformat() + "Z"
end_time = (datetime.utcnow() + timedelta(hours=8)).isoformat() + "Z"
for priority, list_id in list_ids.items():
payload = {
"name": f"{prefix}_campaign_{priority}",
"dialType": "predictive",
"listId": list_id,
"abandonThreshold": threshold_map[priority],
"startTime": start_time,
"endTime": end_time,
"wrapUpCode": "Campaign Complete",
"status": "active",
"predictiveDialingSettings": {
"targetAnswerRate": 0.85,
"maxCallsInProgress": 50,
"agentAvailabilityFactor": 0.9
}
}
response = client._make_request("POST", "/api/v1/campaigns", json=payload)
if response.status_code == 429:
logger.warning("Rate limited on campaign creation. Retrying...")
time.sleep(2)
continue
response.raise_for_status()
campaign_ids[priority] = response.json()["id"]
logger.info(f"Launched campaign {campaign_ids[priority]} with {threshold_map[priority]}% abandon threshold.")
return campaign_ids
The abandonThreshold field accepts a percentage value. Predictive dialing requires predictiveDialingSettings to define answer rate targets and concurrency limits. The status field set to active immediately queues the campaign for execution. CXone validates list population before activation, so list assignment must complete successfully.
Complete Working Example
The following script integrates all components into a single executable module. Replace the placeholder credentials before execution.
import time
import logging
import requests
from typing import Dict, List, Optional
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
from datetime import datetime, timedelta
from collections import defaultdict
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
class CXoneClient:
def __init__(self, tenant_url: str, client_id: str, client_secret: str):
self.tenant_url = tenant_url.rstrip("/")
self.client_id = client_id
self.client_secret = client_secret
self.token: Optional[str] = None
self.token_expiry: float = 0.0
self.session = self._build_session()
def _build_session(self) -> requests.Session:
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1.5,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "PATCH"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
def _get_token(self) -> str:
if self.token and time.time() < self.token_expiry - 300:
return self.token
url = f"{self.tenant_url}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "interactions:read lists:read lists:write campaigns:read campaigns:write"
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = self.session.post(url, data=payload, headers=headers)
response.raise_for_status()
data = response.json()
self.token = data["access_token"]
self.token_expiry = time.time() + data["expires_in"]
return self.token
def _make_request(self, method: str, path: str, **kwargs) -> requests.Response:
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {self._get_token()}"
headers["Content-Type"] = "application/json"
url = f"{self.tenant_url}{path}"
return self.session.request(method, url, headers=headers, **kwargs)
def fetch_interactions(client: CXoneClient, contact_ids: List[str], days_back: int = 90) -> List[Dict]:
start_date = (datetime.utcnow() - timedelta(days=days_back)).isoformat() + "Z"
end_date = datetime.utcnow().isoformat() + "Z"
all_interactions = []
next_token = None
while True:
payload = {
"filters": {
"contactIds": contact_ids,
"startTime": start_date,
"endTime": end_date,
"interactionType": ["voice"]
},
"pageSize": 500,
"pageToken": next_token
}
response = client._make_request("POST", "/api/v1/interactions/search", json=payload)
if response.status_code == 429:
logger.warning("Rate limited on interaction query. Retrying...")
time.sleep(2)
continue
response.raise_for_status()
data = response.json()
all_interactions.extend(data.get("data", []))
next_token = data.get("pagination", {}).get("nextPageToken")
if not next_token:
break
return all_interactions
def calculate_segment_scores(interactions: List[Dict]) -> Dict[str, Dict]:
contact_metrics = defaultdict(lambda: {"frequency": 0, "last_interaction": None, "score": 0.0, "priority": "low"})
now = datetime.utcnow()
for interaction in interactions:
contact_id = interaction.get("contactId")
start_time_str = interaction.get("startTime")
if not contact_id or not start_time_str:
continue
start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00"))
days_since = (now - start_time).total_seconds() / 86400.0
metrics = contact_metrics[contact_id]
metrics["frequency"] += 1
if metrics["last_interaction"] is None or start_time > metrics["last_interaction"]:
metrics["last_interaction"] = start_time
metrics["days_since"] = days_since
for contact_id, metrics in contact_metrics.items():
recency_score = max(0, 50 - (metrics["days_since"] * 0.5))
freq_score = min(metrics["frequency"], 20) * 2.5
metrics["score"] = recency_score + freq_score
metrics["priority"] = "high" if metrics["score"] >= 70 else ("medium" if metrics["score"] >= 40 else "low")
return dict(contact_metrics)
def create_segment_lists(client: CXoneClient, prefix: str) -> Dict[str, str]:
list_ids = {}
for priority in ["high", "medium", "low"]:
payload = {"name": f"{prefix}_segment_{priority}", "description": f"Dynamic segment for {priority} priority contacts", "type": "outbound"}
response = client._make_request("POST", "/api/v1/lists", json=payload)
response.raise_for_status()
list_ids[priority] = response.json()["id"]
return list_ids
def assign_contacts_to_lists(client: CXoneClient, list_ids: Dict[str, str], metrics: Dict[str, Dict]) -> None:
batches = {k: [] for k in list_ids}
for contact_id, data in metrics.items():
batches[data["priority"]].append({"contactId": contact_id, "data": {"priority": data["priority"], "score": data["score"]}})
for priority, contacts in batches.items():
list_id = list_ids[priority]
for i in range(0, len(contacts), 500):
batch = contacts[i:i+500]
response = client._make_request("POST", f"/api/v1/lists/{list_id}/contacts", json=batch)
if response.status_code == 429:
time.sleep(2)
continue
response.raise_for_status()
def launch_predictive_campaigns(client: CXoneClient, list_ids: Dict[str, str], prefix: str) -> Dict[str, str]:
threshold_map = {"high": 3.0, "medium": 5.0, "low": 8.0}
campaign_ids = {}
start_time = (datetime.utcnow() + timedelta(minutes=15)).isoformat() + "Z"
end_time = (datetime.utcnow() + timedelta(hours=8)).isoformat() + "Z"
for priority, list_id in list_ids.items():
payload = {
"name": f"{prefix}_campaign_{priority}", "dialType": "predictive", "listId": list_id,
"abandonThreshold": threshold_map[priority], "startTime": start_time, "endTime": end_time,
"wrapUpCode": "Campaign Complete", "status": "active",
"predictiveDialingSettings": {"targetAnswerRate": 0.85, "maxCallsInProgress": 50, "agentAvailabilityFactor": 0.9}
}
response = client._make_request("POST", "/api/v1/campaigns", json=payload)
if response.status_code == 429:
time.sleep(2)
continue
response.raise_for_status()
campaign_ids[priority] = response.json()["id"]
return campaign_ids
if __name__ == "__main__":
TENANT_URL = "https://api.mynicecx.com"
CLIENT_ID = "YOUR_CLIENT_ID"
CLIENT_SECRET = "YOUR_CLIENT_SECRET"
CONTACT_IDS = ["contact_001", "contact_002", "contact_003"]
PREFIX = "dynamic_outbound"
client = CXoneClient(TENANT_URL, CLIENT_ID, CLIENT_SECRET)
interactions = fetch_interactions(client, CONTACT_IDS, days_back=90)
metrics = calculate_segment_scores(interactions)
list_ids = create_segment_lists(client, PREFIX)
assign_contacts_to_lists(client, list_ids, metrics)
campaign_ids = launch_predictive_campaigns(client, list_ids, PREFIX)
logger.info("Workflow complete. Campaign IDs: %s", campaign_ids)
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Expired OAuth token or invalid client credentials.
- Fix: Verify the client ID and secret match the CXone OAuth application configuration. Ensure the token refresh logic runs before expiration. The provided client automatically refreshes tokens 300 seconds before expiry.
- Code Fix: Replace the hardcoded credentials with environment variables or a secrets manager. Never commit secrets to version control.
Error: 403 Forbidden
- Cause: Missing OAuth scopes or insufficient API permissions for the client application.
- Fix: Navigate to the CXone OAuth application settings and append
interactions:read,lists:write,campaigns:writeto the allowed scopes. Restart the script to trigger a fresh token request with the updated scope string.
Error: 429 Too Many Requests
- Cause: Exceeding tenant-level rate limits during bulk list assignments or interaction queries.
- Fix: The
HTTPAdapterwithRetrystrategy handles automatic backoff. If persistent 429 errors occur, reduce batch sizes from 500 to 200 contacts per list assignment call. Add a fixed delay between campaign creation requests usingtime.sleep(1).
Error: 400 Bad Request on Campaign Creation
- Cause: Invalid
abandonThresholdvalue or missing required predictive dialing parameters. - Fix: Ensure
abandonThresholdis a float between 1.0 and 10.0. VerifypredictiveDialingSettingsincludestargetAnswerRateandmaxCallsInProgress. CXone rejects campaigns that reference empty lists. Confirm list assignment completes before campaign creation.
Error: Pagination Token Exhaustion
- Cause: The Interaction API returns stale or corrupted
nextPageTokenvalues after network interruptions. - Fix: Implement a maximum iteration counter to prevent infinite loops. Reset
next_tokentoNoneand re-query from the start if the response lacks adataarray. Log the total pages processed for audit trails.