Extracting Real-Time Queue Statistics from NICE CXone v2 Reporting API
What You Will Build
- A Python script that authenticates with NICE CXone and retrieves live queue metrics such as wait times, agent availability, and conversation counts.
- The solution utilizes the NICE CXone
v2Reporting API, specifically theGET /api/v2/reporting/queues/statsendpoint. - The implementation uses Python 3.8+ with the
requestslibrary for HTTP handling andpydanticfor data validation.
Prerequisites
- OAuth Client Type: Service Account or Public Client. For automated scripts, a Service Account is recommended to avoid interactive login prompts.
- Required Scopes:
reports:vieworreports:read(depending on your tenant configuration,reports:viewis standard for read-only access to reporting data). - SDK/API Version: NICE CXone OpenAPI v2.
- Language/Runtime: Python 3.8 or higher.
- External Dependencies:
requests: For HTTP communication.pydantic: For robust response parsing (optional but recommended for production).- Install via:
pip install requests pydantic
Authentication Setup
NICE CXone uses OAuth 2.0 for authentication. For backend integrations, the Client Credentials Grant flow is the most reliable method. This flow exchanges a Client ID and Client Secret for an access token.
The token is valid for a limited duration (typically 3600 seconds). A production-grade implementation must cache the token and refresh it only when expired or when a 401 Unauthorized response is received.
Step 1: Implementing the OAuth Token Fetcher
This class handles the initial token acquisition and provides a method to retrieve a valid token. It includes basic caching logic.
import requests
import time
from typing import Optional
class CXoneAuthenticator:
"""
Handles OAuth2 Client Credentials flow for NICE CXone.
"""
def __init__(self, client_id: str, client_secret: str, base_url: str = "https://api-us.nice-incontact.com"):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url
self.token_url = f"{base_url}/v2/oauth2/token"
# Internal cache for the token
self.access_token: Optional[str] = None
self.token_expiry: float = 0.0
def get_token(self) -> str:
"""
Returns a valid access token.
If the current token is expired or missing, it fetches a new one.
"""
# Check if we have a valid token
if self.access_token and time.time() < self.token_expiry:
return self.access_token
# Fetch new token
token_data = self._fetch_oauth_token()
# Update cache
self.access_token = token_data['access_token']
# Expires_in is in seconds, add to current time
self.token_expiry = time.time() + token_data['expires_in']
return self.access_token
def _fetch_oauth_token(self) -> dict:
"""
Performs the POST request to the OAuth endpoint.
"""
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
try:
response = requests.post(self.token_url, data=payload, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("OAuth Authentication Failed: Invalid Client ID or Secret.") from e
elif response.status_code == 400:
raise Exception("OAuth Request Malformed: Check payload structure.") from e
else:
raise Exception(f"Unexpected OAuth Error: {response.status_code} - {response.text}") from e
except requests.exceptions.RequestException as e:
raise Exception(f"Network error during OAuth token fetch: {str(e)}") from e
Implementation
Step 2: Constructing the Queue Stats Request
The endpoint GET /api/v2/reporting/queues/stats accepts several query parameters to filter and shape the data.
Critical Parameters:
queueIds: A comma-separated list of Queue IDs. If omitted, it returns stats for all queues (which can be large).granularity: For real-time stats, useREAL_TIME.interval: For real-time, this is often ignored or set to a small window, butREAL_TIMEgranularity implies the current state.metricTypes: A comma-separated list of metrics to retrieve (e.g.,waitTime,agentCount,conversationCount). Specifying only needed metrics reduces payload size.
OAuth Scope Required: reports:view
import requests
from typing import List, Dict, Any
class CXoneQueueStats:
"""
Retrieves real-time statistics for NICE CXone queues.
"""
def __init__(self, authenticator: CXoneAuthenticator):
self.auth = authenticator
self.base_url = authenticator.base_url
self.endpoint = f"{self.base_url}/api/v2/reporting/queues/stats"
def get_real_time_stats(
self,
queue_ids: List[str],
metric_types: List[str] = None
) -> Dict[str, Any]:
"""
Fetches real-time statistics for specific queues.
Args:
queue_ids: List of Queue IDs (UUIDs) to query.
metric_types: Optional list of metrics (e.g., ['waitTime', 'agentCount']).
If None, defaults to common real-time metrics.
Returns:
JSON response from the API.
"""
# Default metrics if not specified
if not metric_types:
metric_types = [
"waitTime",
"agentCount",
"conversationCount",
"abandonCount",
"serviceLevel"
]
# Prepare Query Parameters
# The API expects comma-separated strings for lists
params = {
"queueIds": ",".join(queue_ids),
"granularity": "REAL_TIME",
"metricTypes": ",".join(metric_types)
}
# Get valid token
token = self.auth.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json"
}
try:
response = requests.get(self.endpoint, headers=headers, params=params)
# Handle 429 Too Many Requests
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 5))
print(f"Rate limited. Retrying after {retry_after} seconds...")
time.sleep(retry_after)
# Recursive retry with limited depth in production, simplified here
response = requests.get(self.endpoint, headers=headers, params=params)
response.raise_for_status()
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if response.status_code == 403:
raise Exception("Forbidden: Check OAuth Scopes. Ensure 'reports:view' is granted.") from e
elif response.status_code == 404:
raise Exception("Not Found: One or more Queue IDs do not exist.") from e
else:
raise Exception(f"HTTP Error: {response.status_code} - {response.text}") from e
except requests.exceptions.RequestException as e:
raise Exception(f"Network Error: {str(e)}") from e
Step 3: Processing and Parsing the Response
The response from /api/v2/reporting/queues/stats is a nested structure. When granularity is REAL_TIME, the response contains a data array where each object represents a queue’s current state.
Response Structure Analysis:
data: Array of queue objects.queue: Object containingidandname.metrics: Array of metric objects.name: The metric name (e.g.,waitTime).values: Array of value objects. ForREAL_TIME, this usually contains a single object withvalueandtimestamp.
We need a parser to flatten this structure into a usable dictionary for the application layer.
def parse_queue_stats_response(response_data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
"""
Parses the raw JSON response into a flat dictionary keyed by Queue ID.
Returns:
{
"queue_id_1": {
"name": "Sales Support",
"waitTime": 12.5,
"agentCount": 5,
...
},
"queue_id_2": { ... }
}
"""
parsed_stats = {}
if "data" not in response_data:
return parsed_stats
for queue_data in response_data["data"]:
queue_id = queue_data.get("queue", {}).get("id")
queue_name = queue_data.get("queue", {}).get("name")
if not queue_id:
continue
# Initialize entry for this queue
queue_entry = {
"queueId": queue_id,
"queueName": queue_name,
"timestamp": None
}
# Extract metrics
metrics = queue_data.get("metrics", [])
last_timestamp = None
for metric in metrics:
metric_name = metric.get("name")
values = metric.get("values", [])
if values:
# For REAL_TIME, we typically take the first/last value as the current state
# The API returns an array of values over the interval, but for REAL_TIME it is often a single point
current_value = values[0].get("value")
value_timestamp = values[0].get("timestamp")
if value_timestamp:
last_timestamp = value_timestamp
# Store the metric in the flat structure
queue_entry[metric_name] = current_value
queue_entry["timestamp"] = last_timestamp
parsed_stats[queue_id] = queue_entry
return parsed_stats
Complete Working Example
This script combines authentication, data fetching, and parsing. It demonstrates how to query a specific set of queues and print their real-time status.
import sys
import os
import json
from datetime import datetime
# Import the classes defined in previous sections
# In a real project, these would be in separate modules
# from auth import CXoneAuthenticator
# from reporting import CXoneQueueStats, parse_queue_stats_response
# Re-defining here for copy-paste completeness
import requests
import time
from typing import Optional, List, Dict, Any
class CXoneAuthenticator:
def __init__(self, client_id: str, client_secret: str, base_url: str = "https://api-us.nice-incontact.com"):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url
self.token_url = f"{base_url}/v2/oauth2/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0.0
def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry:
return self.access_token
token_data = self._fetch_oauth_token()
self.access_token = token_data['access_token']
self.token_expiry = time.time() + token_data['expires_in']
return self.access_token
def _fetch_oauth_token(self) -> dict:
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
try:
response = requests.post(self.token_url, data=payload, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
raise Exception(f"OAuth Error: {response.status_code} - {response.text}") from e
class CXoneQueueStats:
def __init__(self, authenticator: CXoneAuthenticator):
self.auth = authenticator
self.base_url = authenticator.base_url
self.endpoint = f"{self.base_url}/api/v2/reporting/queues/stats"
def get_real_time_stats(self, queue_ids: List[str], metric_types: List[str] = None) -> Dict[str, Any]:
if not metric_types:
metric_types = ["waitTime", "agentCount", "conversationCount", "abandonCount"]
params = {
"queueIds": ",".join(queue_ids),
"granularity": "REAL_TIME",
"metricTypes": ",".join(metric_types)
}
token = self.auth.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json"
}
try:
response = requests.get(self.endpoint, headers=headers, params=params)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 5))
time.sleep(retry_after)
response = requests.get(self.endpoint, headers=headers, params=params)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
raise Exception(f"API Error: {response.status_code} - {response.text}") from e
def parse_queue_stats_response(response_data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
parsed_stats = {}
if "data" not in response_data:
return parsed_stats
for queue_data in response_data["data"]:
queue_id = queue_data.get("queue", {}).get("id")
queue_name = queue_data.get("queue", {}).get("name")
if not queue_id:
continue
queue_entry = {
"queueId": queue_id,
"queueName": queue_name,
"timestamp": None
}
metrics = queue_data.get("metrics", [])
last_timestamp = None
for metric in metrics:
metric_name = metric.get("name")
values = metric.get("values", [])
if values:
current_value = values[0].get("value")
value_timestamp = values[0].get("timestamp")
if value_timestamp:
last_timestamp = value_timestamp
queue_entry[metric_name] = current_value
queue_entry["timestamp"] = last_timestamp
parsed_stats[queue_id] = queue_entry
return parsed_stats
def main():
# 1. Configuration
CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
# Example Queue IDs - Replace with actual IDs from your tenant
# You can find these in the CXone Admin Portal under IVR/Queue Management
QUEUE_IDS = [
"12345678-1234-1234-1234-1234567890ab",
"87654321-4321-4321-4321-ba0987654321"
]
if not CLIENT_ID or not CLIENT_SECRET:
print("Error: CXONE_CLIENT_ID and CXONE_CLIENT_SECRET environment variables are required.")
sys.exit(1)
# 2. Initialize Components
try:
authenticator = CXoneAuthenticator(CLIENT_ID, CLIENT_SECRET)
stats_client = CXoneQueueStats(authenticator)
except Exception as e:
print(f"Initialization Error: {e}")
sys.exit(1)
# 3. Fetch Data
try:
print("Fetching real-time queue statistics...")
raw_response = stats_client.get_real_time_stats(QUEUE_IDS)
# 4. Parse Data
formatted_stats = parse_queue_stats_response(raw_response)
# 5. Output Results
if not formatted_stats:
print("No data returned for the specified queues.")
return
for queue_id, stats in formatted_stats.items():
print(f"\n--- Queue: {stats['queueName']} ({queue_id}) ---")
print(f"Timestamp: {stats['timestamp']}")
# Display specific metrics
wait_time = stats.get('waitTime', 'N/A')
agent_count = stats.get('agentCount', 'N/A')
conv_count = stats.get('conversationCount', 'N/A')
abandon_count = stats.get('abandonCount', 'N/A')
print(f" Wait Time: {wait_time} seconds")
print(f" Agents Available: {agent_count}")
print(f" Active Conversations: {conv_count}")
print(f" Abandons: {abandon_count}")
except Exception as e:
print(f"Error during execution: {e}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is expired, invalid, or the Client ID/Secret is incorrect.
- Fix: Verify the credentials in the NICE CXone Admin Portal under Manage > Settings > Clients. Ensure the token refresh logic is active. If using a cached token, check if
time.time() < self.token_expirylogic is correct.
Error: 403 Forbidden
- Cause: The OAuth Client does not have the required scope.
- Fix: Go to Manage > Settings > Clients, edit your client, and ensure the
reports:viewscope is checked. Note that changing scopes requires re-authenticating (getting a new token).
Error: 429 Too Many Requests
- Cause: You have exceeded the API rate limit for your tenant or client.
- Fix: Implement exponential backoff. The
Retry-Afterheader indicates how many seconds to wait. The code example above includes a basic retry mechanism for 429 responses.
Error: 400 Bad Request
- Cause: Invalid query parameters.
- Fix: Check the
queueIdsparameter. Ensure IDs are valid UUIDs. Ensuregranularityis exactlyREAL_TIME(case-sensitive). EnsuremetricTypescontains valid metric names.
Error: Empty Response or Missing Metrics
- Cause: The queues specified do not have any activity, or the metric types requested are not supported for
REAL_TIMEgranularity. - Fix: Verify that the Queue IDs exist and are active. Some metrics (like historical averages) are not available in
REAL_TIMEgranularity. Stick to current state metrics likewaitTime,agentCount, andconversationCount.