Extracting Real-Time Queue Statistics from NICE CXone v2 Reporting API

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 v2 Reporting API, specifically the GET /api/v2/reporting/queues/stats endpoint.
  • The implementation uses Python 3.8+ with the requests library for HTTP handling and pydantic for 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:view or reports:read (depending on your tenant configuration, reports:view is 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, use REAL_TIME.
  • interval: For real-time, this is often ignored or set to a small window, but REAL_TIME granularity 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 containing id and name.
  • metrics: Array of metric objects.
    • name: The metric name (e.g., waitTime).
    • values: Array of value objects. For REAL_TIME, this usually contains a single object with value and timestamp.

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_expiry logic 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:view scope 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-After header 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 queueIds parameter. Ensure IDs are valid UUIDs. Ensure granularity is exactly REAL_TIME (case-sensitive). Ensure metricTypes contains 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_TIME granularity.
  • Fix: Verify that the Queue IDs exist and are active. Some metrics (like historical averages) are not available in REAL_TIME granularity. Stick to current state metrics like waitTime, agentCount, and conversationCount.

Official References