Optimizing NICE CXone Workforce Management Schedules via REST API with Python SDK

Optimizing NICE CXone Workforce Management Schedules via REST API with Python SDK

What You Will Build

  • A Python module that constructs, validates, and submits optimized WFM schedule payloads to NICE CXone using shift ID references, skill coverage matrices, and break allocation directives.
  • The implementation uses the CXone REST API surface and Python SDK for atomic roster generation, union rule enforcement, overtime threshold verification, webhook synchronization, and labor cost calculation triggers.
  • All code is written in Python 3.9+ using requests for HTTP transport and production-ready error handling patterns.

Prerequisites

  • OAuth 2.0 Client Credentials flow with scopes: wfm:schedules:write, wfm:optimization:execute, wfm:rosters:read, analytics:write, webhooks:manage
  • CXone API region endpoint (e.g., us-1.api.nicecxone.com or eu-1.api.nicecxone.com)
  • Python 3.9 or higher
  • Install dependencies: pip install requests cxone-python-sdk python-dateutil
  • A configured WFM optimization engine ID and roster ID from your CXone tenant

Authentication Setup

CXone uses OAuth 2.0 Client Credentials for server-to-server API access. The token manager below handles initial retrieval, automatic refresh before expiration, and exponential backoff for rate limits.

import requests
import time
import threading
from typing import Optional
from datetime import datetime, timezone, timedelta

class CxoneTokenManager:
    def __init__(self, client_id: str, client_secret: str, region: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.region = region
        self.token_url = f"https://{region}.api.nicecxone.com/oauth/token"
        self.access_token: Optional[str] = None
        self.expires_at: Optional[datetime] = None
        self._lock = threading.Lock()

    def _fetch_token(self) -> dict:
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": "wfm:schedules:write wfm:optimization:execute wfm:rosters:read analytics:write webhooks:manage"
        }
        response = requests.post(self.token_url, headers=headers, data=data)
        response.raise_for_status()
        return response.json()

    def get_token(self) -> str:
        with self._lock:
            if self.access_token and self.expires_at and datetime.now(timezone.utc) < self.expires_at:
                return self.access_token

            retries = 3
            for attempt in range(retries):
                try:
                    token_data = self._fetch_token()
                    self.access_token = token_data["access_token"]
                    expires_in = token_data.get("expires_in", 3600)
                    self.expires_at = datetime.now(timezone.utc) + timedelta(seconds=expires_in)
                    return self.access_token
                except requests.exceptions.HTTPError as e:
                    if e.response.status_code == 429:
                        wait_time = 2 ** attempt
                        time.sleep(wait_time)
                    else:
                        raise
            raise RuntimeError("Failed to acquire OAuth token after retries")

OAuth Scope Requirement: wfm:schedules:write wfm:optimization:execute wfm:rosters:read analytics:write webhooks:manage
Expected Response: JSON containing access_token, token_type, expires_in
Error Handling: The manager catches 429 Too Many Requests with exponential backoff. Any 4xx or 5xx raises requests.exceptions.HTTPError for upstream handling.

Implementation

Step 1: Construct Schedule Payloads with Shift References and Coverage Matrices

The optimization payload must explicitly reference existing shift templates, define skill coverage requirements, and allocate breaks. The CXone WFM API expects a structured JSON body that maps agents to shifts while respecting capacity matrices.

import json
from datetime import datetime, timezone

def build_optimization_payload(
    optimization_id: str,
    agents: list[dict],
    shift_templates: list[dict],
    skill_matrix: dict[str, dict],
    break_rules: list[dict],
    period_start: str,
    period_end: str
) -> dict:
    schedule_assignments = []
    
    for agent in agents:
        agent_id = agent["id"]
        assigned_shift = next((s for s in shift_templates if s["id"] == agent["shift_id"]), None)
        if not assigned_shift:
            raise ValueError(f"Shift template {agent['shift_id']} not found")

        coverage_requirements = []
        for skill_id, req in skill_matrix.items():
            if agent.get("skills", {}).get(skill_id):
                coverage_requirements.append({
                    "skillId": skill_id,
                    "requiredCapacity": req["capacity"],
                    "agentId": agent_id
                })

        break_allocations = []
        for rule in break_rules:
            break_allocations.append({
                "breakId": rule["id"],
                "startTime": agent.get("break_start", "2024-01-01T12:00:00Z"),
                "durationMinutes": rule["duration_minutes"]
            })

        schedule_assignments.append({
            "agentId": agent_id,
            "shiftId": assigned_shift["id"],
            "shiftTemplateId": assigned_shift["templateId"],
            "skillCoverage": coverage_requirements,
            "breakAllocations": break_allocations,
            "status": "PROPOSED"
        })

    return {
        "optimizationId": optimization_id,
        "schedulePeriod": {
            "start": period_start,
            "end": period_end
        },
        "scheduleAssignments": schedule_assignments,
        "metadata": {
            "generatedBy": "wfm-optimizer-py",
            "timestamp": datetime.now(timezone.utc).isoformat()
        }
    }

OAuth Scope Requirement: wfm:schedules:write
Expected Response: Python dictionary ready for JSON serialization
Error Handling: Raises ValueError if shift templates or skill mappings are missing. The calling layer must catch this before API submission to prevent 400 Bad Request responses.

Step 2: Validate Schemas Against Planning Constraints and Duration Limits

Union rules and overtime thresholds must be verified before submission. The validation pipeline enforces maximum shift duration, minimum rest periods, consecutive day limits, and weekly overtime caps. This prevents optimization failure at the API boundary.

from dateutil.parser import isoparse
from datetime import timedelta

def validate_schedule_constraints(payload: dict) -> list[str]:
    violations = []
    max_shift_hours = 12
    max_weekly_hours = 45
    min_rest_hours = 8
    max_consecutive_days = 6

    assignments = payload.get("scheduleAssignments", [])
    agent_schedules: dict[str, list] = {}

    for assignment in assignments:
        agent_id = assignment["agentId"]
        shift_id = assignment["shiftId"]
        
        if agent_id not in agent_schedules:
            agent_schedules[agent_id] = []
        agent_schedules[agent_id].append(assignment)

    for agent_id, schedules in agent_schedules.items():
        total_weekly_hours = 0
        consecutive_days = 0
        last_end_time = None

        sorted_schedules = sorted(schedules, key=lambda x: x.get("shiftStart", "2024-01-01T00:00:00Z"))

        for schedule in sorted_schedules:
            start_str = schedule.get("shiftStart", "2024-01-01T08:00:00Z")
            end_str = schedule.get("shiftEnd", "2024-01-01T16:00:00Z")
            start_dt = isoparse(start_str)
            end_dt = isoparse(end_str)
            duration_hours = (end_dt - start_dt).total_seconds() / 3600

            if duration_hours > max_shift_hours:
                violations.append(f"Agent {agent_id} exceeds max shift duration on {start_str}")

            total_weekly_hours += duration_hours
            if total_weekly_hours > max_weekly_hours:
                violations.append(f"Agent {agent_id} exceeds weekly overtime threshold")

            if last_end_time:
                rest_gap = (start_dt - last_end_time).total_seconds() / 3600
                if rest_gap < min_rest_hours:
                    violations.append(f"Agent {agent_id} violates minimum rest period before {start_str}")

            consecutive_days += 1
            if consecutive_days > max_consecutive_days:
                violations.append(f"Agent {agent_id} violates union consecutive day limit")

            last_end_time = end_dt

    return violations

OAuth Scope Requirement: None (client-side validation)
Expected Response: List of violation strings. Empty list indicates compliance.
Error Handling: Returns structured violations instead of raising exceptions. The caller must abort the PUT request if the list is non-empty to preserve API rate limits and maintain audit integrity.

Step 3: Execute Atomic Roster Generation with Cost Calculation Triggers

Roster submission requires an atomic PUT operation. The endpoint accepts the validated payload and returns a synchronous response. Adding the triggerCostCalculation=true query parameter initiates automatic labor cost evaluation. The request includes format verification headers and handles pagination metadata when processing large rosters.

import requests

def submit_roster_optimization(
    base_url: str,
    token: str,
    roster_id: str,
    payload: dict,
    trigger_cost_calc: bool = True
) -> dict:
    endpoint = f"{base_url}/api/v2/wfm/rosters/{roster_id}/schedules"
    params = {"triggerCostCalculation": str(trigger_cost_calc).lower()}
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json",
        "X-Request-Id": f"opt-{roster_id}-{int(time.time())}"
    }

    retries = 3
    for attempt in range(retries):
        try:
            response = requests.put(
                endpoint,
                headers=headers,
                json=payload,
                params=params,
                timeout=30
            )
            
            if response.status_code == 429:
                wait_time = 2 ** attempt
                time.sleep(wait_time)
                continue
            
            response.raise_for_status()
            
            result = response.json()
            if result.get("status") != "COMPLETED":
                raise RuntimeError(f"Optimization returned status: {result.get('status')}")
            
            return result
            
        except requests.exceptions.HTTPError as e:
            if e.response.status_code in (401, 403):
                raise RuntimeError("Authentication or authorization failed. Verify OAuth scopes.") from e
            if e.response.status_code == 400:
                raise ValueError(f"Payload validation failed: {e.response.text}") from e
            if e.response.status_code >= 500:
                if attempt == retries - 1:
                    raise RuntimeError("Server error after retries") from e
                time.sleep(2 ** attempt)
                continue
            raise
    raise RuntimeError("Maximum retries exceeded")

OAuth Scope Requirement: wfm:schedules:write wfm:rosters:read
Expected Response: JSON containing status, scheduleId, laborCostEstimate, coverageAccuracy, pagination (if applicable)
Error Handling: Handles 429 with backoff, 401/403 with explicit scope verification, 400 with payload inspection, and 5xx with retry logic. The X-Request-Id header enables trace correlation in CXone logs.

Step 4: Synchronize Events, Track Latency, and Generate Audit Logs

Webhook callbacks align external HR systems with CXone optimization events. The tracking decorator measures latency and coverage accuracy. Audit logs capture every optimization cycle for labor governance compliance.

import json
import logging
from functools import wraps
from datetime import datetime, timezone

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("wfm_optimizer")

def track_optimization_metrics(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        try:
            result = func(*args, **kwargs)
            latency_ms = (time.time() - start_time) * 1000
            coverage_accuracy = result.get("coverageAccuracy", 0.0)
            
            audit_entry = {
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "event": "optimization_submitted",
                "latency_ms": round(latency_ms, 2),
                "coverage_accuracy": coverage_accuracy,
                "labor_cost_estimate": result.get("laborCostEstimate", {}),
                "status": "SUCCESS"
            }
            
            logger.info(json.dumps(audit_entry))
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            audit_entry = {
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "event": "optimization_failed",
                "latency_ms": round(latency_ms, 2),
                "error": str(e),
                "status": "FAILURE"
            }
            logger.error(json.dumps(audit_entry))
            raise
    return wrapper

def register_webhook_callback(base_url: str, token: str, callback_url: str) -> dict:
    endpoint = f"{base_url}/api/v2/wfm/webhooks"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    payload = {
        "name": "hr-schedule-sync",
        "url": callback_url,
        "events": ["SCHEDULE_CREATED", "SCHEDULE_UPDATED", "OPTIMIZATION_COMPLETED"],
        "secret": "wfm-webhook-secret-key",
        "active": True
    }
    
    response = requests.post(endpoint, headers=headers, json=payload)
    response.raise_for_status()
    return response.json()

OAuth Scope Requirement: webhooks:manage analytics:write
Expected Response: Webhook registration JSON with id, url, status
Error Handling: Captures exceptions in the decorator, logs structured JSON audit entries, and preserves the original exception for upstream handling. Webhook registration handles 409 Conflict implicitly via raise_for_status().

Complete Working Example

The following script combines all components into a production-ready optimizer. Replace placeholder credentials and IDs with your tenant values.

import requests
import time
import threading
import json
import logging
from datetime import datetime, timezone, timedelta
from dateutil.parser import isoparse
from functools import wraps
from typing import Optional

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("wfm_optimizer")

class CxoneTokenManager:
    def __init__(self, client_id: str, client_secret: str, region: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.region = region
        self.token_url = f"https://{region}.api.nicecxone.com/oauth/token"
        self.access_token: Optional[str] = None
        self.expires_at: Optional[datetime] = None
        self._lock = threading.Lock()

    def _fetch_token(self) -> dict:
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": "wfm:schedules:write wfm:optimization:execute wfm:rosters:read analytics:write webhooks:manage"
        }
        response = requests.post(self.token_url, headers=headers, data=data)
        response.raise_for_status()
        return response.json()

    def get_token(self) -> str:
        with self._lock:
            if self.access_token and self.expires_at and datetime.now(timezone.utc) < self.expires_at:
                return self.access_token
            retries = 3
            for attempt in range(retries):
                try:
                    token_data = self._fetch_token()
                    self.access_token = token_data["access_token"]
                    expires_in = token_data.get("expires_in", 3600)
                    self.expires_at = datetime.now(timezone.utc) + timedelta(seconds=expires_in)
                    return self.access_token
                except requests.exceptions.HTTPError as e:
                    if e.response.status_code == 429:
                        wait_time = 2 ** attempt
                        time.sleep(wait_time)
                    else:
                        raise
            raise RuntimeError("Failed to acquire OAuth token after retries")

def build_optimization_payload(
    optimization_id: str,
    agents: list[dict],
    shift_templates: list[dict],
    skill_matrix: dict[str, dict],
    break_rules: list[dict],
    period_start: str,
    period_end: str
) -> dict:
    schedule_assignments = []
    for agent in agents:
        agent_id = agent["id"]
        assigned_shift = next((s for s in shift_templates if s["id"] == agent["shift_id"]), None)
        if not assigned_shift:
            raise ValueError(f"Shift template {agent['shift_id']} not found")
        coverage_requirements = []
        for skill_id, req in skill_matrix.items():
            if agent.get("skills", {}).get(skill_id):
                coverage_requirements.append({
                    "skillId": skill_id,
                    "requiredCapacity": req["capacity"],
                    "agentId": agent_id
                })
        break_allocations = []
        for rule in break_rules:
            break_allocations.append({
                "breakId": rule["id"],
                "startTime": agent.get("break_start", "2024-01-01T12:00:00Z"),
                "durationMinutes": rule["duration_minutes"]
            })
        schedule_assignments.append({
            "agentId": agent_id,
            "shiftId": assigned_shift["id"],
            "shiftTemplateId": assigned_shift["templateId"],
            "skillCoverage": coverage_requirements,
            "breakAllocations": break_allocations,
            "status": "PROPOSED"
        })
    return {
        "optimizationId": optimization_id,
        "schedulePeriod": {"start": period_start, "end": period_end},
        "scheduleAssignments": schedule_assignments,
        "metadata": {"generatedBy": "wfm-optimizer-py", "timestamp": datetime.now(timezone.utc).isoformat()}
    }

def validate_schedule_constraints(payload: dict) -> list[str]:
    violations = []
    max_shift_hours = 12
    max_weekly_hours = 45
    min_rest_hours = 8
    max_consecutive_days = 6
    assignments = payload.get("scheduleAssignments", [])
    agent_schedules: dict[str, list] = {}
    for assignment in assignments:
        agent_id = assignment["agentId"]
        if agent_id not in agent_schedules:
            agent_schedules[agent_id] = []
        agent_schedules[agent_id].append(assignment)
    for agent_id, schedules in agent_schedules.items():
        total_weekly_hours = 0
        consecutive_days = 0
        last_end_time = None
        sorted_schedules = sorted(schedules, key=lambda x: x.get("shiftStart", "2024-01-01T08:00:00Z"))
        for schedule in sorted_schedules:
            start_str = schedule.get("shiftStart", "2024-01-01T08:00:00Z")
            end_str = schedule.get("shiftEnd", "2024-01-01T16:00:00Z")
            start_dt = isoparse(start_str)
            end_dt = isoparse(end_str)
            duration_hours = (end_dt - start_dt).total_seconds() / 3600
            if duration_hours > max_shift_hours:
                violations.append(f"Agent {agent_id} exceeds max shift duration on {start_str}")
            total_weekly_hours += duration_hours
            if total_weekly_hours > max_weekly_hours:
                violations.append(f"Agent {agent_id} exceeds weekly overtime threshold")
            if last_end_time:
                rest_gap = (start_dt - last_end_time).total_seconds() / 3600
                if rest_gap < min_rest_hours:
                    violations.append(f"Agent {agent_id} violates minimum rest period before {start_str}")
            consecutive_days += 1
            if consecutive_days > max_consecutive_days:
                violations.append(f"Agent {agent_id} violates union consecutive day limit")
            last_end_time = end_dt
    return violations

def track_optimization_metrics(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        try:
            result = func(*args, **kwargs)
            latency_ms = (time.time() - start_time) * 1000
            coverage_accuracy = result.get("coverageAccuracy", 0.0)
            audit_entry = {
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "event": "optimization_submitted",
                "latency_ms": round(latency_ms, 2),
                "coverage_accuracy": coverage_accuracy,
                "labor_cost_estimate": result.get("laborCostEstimate", {}),
                "status": "SUCCESS"
            }
            logger.info(json.dumps(audit_entry))
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            audit_entry = {
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "event": "optimization_failed",
                "latency_ms": round(latency_ms, 2),
                "error": str(e),
                "status": "FAILURE"
            }
            logger.error(json.dumps(audit_entry))
            raise
    return wrapper

@track_optimization_metrics
def submit_roster_optimization(base_url: str, token: str, roster_id: str, payload: dict) -> dict:
    endpoint = f"{base_url}/api/v2/wfm/rosters/{roster_id}/schedules"
    params = {"triggerCostCalculation": "true"}
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json",
        "X-Request-Id": f"opt-{roster_id}-{int(time.time())}"
    }
    retries = 3
    for attempt in range(retries):
        try:
            response = requests.put(endpoint, headers=headers, json=payload, params=params, timeout=30)
            if response.status_code == 429:
                wait_time = 2 ** attempt
                time.sleep(wait_time)
                continue
            response.raise_for_status()
            result = response.json()
            if result.get("status") != "COMPLETED":
                raise RuntimeError(f"Optimization returned status: {result.get('status')}")
            return result
        except requests.exceptions.HTTPError as e:
            if e.response.status_code in (401, 403):
                raise RuntimeError("Authentication or authorization failed. Verify OAuth scopes.") from e
            if e.response.status_code == 400:
                raise ValueError(f"Payload validation failed: {e.response.text}") from e
            if e.response.status_code >= 500:
                if attempt == retries - 1:
                    raise RuntimeError("Server error after retries") from e
                time.sleep(2 ** attempt)
                continue
            raise
    raise RuntimeError("Maximum retries exceeded")

def register_webhook_callback(base_url: str, token: str, callback_url: str) -> dict:
    endpoint = f"{base_url}/api/v2/wfm/webhooks"
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    payload = {
        "name": "hr-schedule-sync",
        "url": callback_url,
        "events": ["SCHEDULE_CREATED", "SCHEDULE_UPDATED", "OPTIMIZATION_COMPLETED"],
        "secret": "wfm-webhook-secret-key",
        "active": True
    }
    response = requests.post(endpoint, headers=headers, json=payload)
    response.raise_for_status()
    return response.json()

if __name__ == "__main__":
    CLIENT_ID = "your_client_id"
    CLIENT_SECRET = "your_client_secret"
    REGION = "us-1"
    BASE_URL = f"https://{REGION}.api.nicecxone.com"
    ROSTER_ID = "roster_12345"
    OPT_ID = "opt_2024_w1"
    CALLBACK_URL = "https://your-hr-system.com/webhooks/cxone-schedules"

    token_mgr = CxoneTokenManager(CLIENT_ID, CLIENT_SECRET, REGION)
    token = token_mgr.get_token()

    agents = [
        {"id": "agt_001", "shift_id": "sh_morning", "skills": {"skl_001": True}, "break_start": "2024-01-01T12:00:00Z"},
        {"id": "agt_002", "shift_id": "sh_evening", "skills": {"skl_002": True}, "break_start": "2024-01-01T16:00:00Z"}
    ]
    shifts = [
        {"id": "sh_morning", "templateId": "tpl_001", "shiftStart": "2024-01-01T08:00:00Z", "shiftEnd": "2024-01-01T16:00:00Z"},
        {"id": "sh_evening", "templateId": "tpl_002", "shiftStart": "2024-01-01T16:00:00Z", "shiftEnd": "2024-01-02T00:00:00Z"}
    ]
    skills = {"skl_001": {"capacity": 2}, "skl_002": {"capacity": 1}}
    breaks = [{"id": "brk_001", "duration_minutes": 30}]

    payload = build_optimization_payload(OPT_ID, agents, shifts, skills, breaks, "2024-01-01T00:00:00Z", "2024-01-07T23:59:59Z")
    violations = validate_schedule_constraints(payload)
    
    if violations:
        print("Validation failed:")
        for v in violations:
            print(v)
    else:
        register_webhook_callback(BASE_URL, token, CALLBACK_URL)
        result = submit_roster_optimization(BASE_URL, token, ROSTER_ID, payload)
        print("Optimization completed successfully")
        print(json.dumps(result, indent=2))

Common Errors & Debugging

Error: 401 Unauthorized

  • What causes it: Expired OAuth token, missing Authorization header, or invalid client credentials.
  • How to fix it: Verify the token manager refreshes before expiration. Check that the Bearer prefix is present. Confirm the client ID and secret match the CXone developer console.
  • Code showing the fix: The CxoneTokenManager automatically refreshes tokens when datetime.now(timezone.utc) >= self.expires_at. Replace hardcoded tokens with dynamic calls to token_mgr.get_token().

Error: 403 Forbidden

  • What causes it: OAuth scopes do not include wfm:schedules:write or wfm:optimization:execute.
  • How to fix it: Update the OAuth client in the CXone admin console to include all required scopes. Revoke and regenerate the token after scope updates.
  • Code showing the fix: Ensure the scope parameter in _fetch_token matches exactly: wfm:schedules:write wfm:optimization:execute wfm:rosters:read analytics:write webhooks:manage.

Error: 400 Bad Request

  • What causes it: Payload schema mismatch, missing required fields, or constraint violations bypassing client-side validation.
  • How to fix it: Run validate_schedule_constraints() before submission. Inspect the e.response.text for field-level errors. Ensure shift IDs and skill IDs exist in the tenant.
  • Code showing the fix: The validation pipeline returns explicit violation strings. Abort the PUT request if len(violations) > 0.

Error: 429 Too Many Requests

  • What causes it: Exceeding CXone API rate limits during bulk roster generation or repeated optimization calls.
  • How to fix it: Implement exponential backoff. Add X-Request-Id for trace correlation. Space out optimization jobs by at least 2 seconds.
  • Code showing the fix: The retry loop in submit_roster_optimization and CxoneTokenManager handles 429 with time.sleep(2 ** attempt).

Error: 500 Internal Server Error

  • What causes it: CXone WFM engine timeout, database lock contention, or unsupported break allocation formats.
  • How to fix it: Retry with backoff. Reduce payload size by splitting large rosters into weekly chunks. Verify break allocations use ISO 8601 timestamps.
  • Code showing the fix: The retry mechanism catches 5xx status codes and retries up to 3 times. If it persists, contact CXone support with the X-Request-Id value.

Official References