CXone Reporting API (v2) — Query Agent State History for the Last 24 Hours

CXone Reporting API (v2) — Query Agent State History for the Last 24 Hours

What You Will Build

  • A Python script that authenticates to NICE CXone, queries the Interaction Analytics API for agent state history, and filters the results to the last 24 hours.
  • This tutorial uses the NICE CXone REST API (v2) via direct HTTP requests, as the official Python SDK has limited coverage for specific reporting endpoints.
  • The programming language used is Python 3.10+ using the requests library.

Prerequisites

  • OAuth Client Type: A Service Account or OAuth Application configured in CXone with the reporting:read scope.
  • API Version: NICE CXone Reporting API v2.
  • Runtime: Python 3.10 or higher.
  • Dependencies: requests (pip install requests).
  • Credentials: Your CXone Domain (e.g., mydomain.nicecxone.com) and OAuth Client ID/Secret.

Authentication Setup

NICE CXone uses standard OAuth 2.0 Client Credentials flow. You must obtain an access token before making any API calls. The token expires after 3600 seconds (1 hour), so production code should cache and refresh tokens. For this tutorial, we will fetch a fresh token on every run to keep the example self-contained.

The endpoint for token acquisition is https://{domain}/api/v2/oauth/token.

import requests
import json
from datetime import datetime, timedelta
from typing import Dict, List, Any

class CXoneAuth:
    def __init__(self, domain: str, client_id: str, client_secret: str):
        self.domain = domain
        self.base_url = f"https://{domain}/api/v2"
        self.client_id = client_id
        self.client_secret = client_secret
        self.access_token = None

    def get_access_token(self) -> str:
        """
        Retrieves an OAuth 2.0 access token using Client Credentials flow.
        """
        token_url = f"https://{self.domain}/api/v2/oauth/token"
        
        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(token_url, data=payload, headers=headers, timeout=10)
            response.raise_for_status()
            
            token_data = response.json()
            self.access_token = token_data.get("access_token")
            
            if not self.access_token:
                raise ValueError("Failed to extract access_token from response.")
                
            return self.access_token

        except requests.exceptions.HTTPError as http_err:
            if response.status_code == 401:
                raise Exception("Authentication failed: Invalid Client ID or Secret.") from http_err
            elif response.status_code == 403:
                raise Exception("Forbidden: Client does not have permission to request tokens.") from http_err
            else:
                raise Exception(f"HTTP Error during token request: {http_err}") from http_err
        except requests.exceptions.RequestException as req_err:
            raise Exception(f"Network error during token request: {req_err}") from req_err

Implementation

Step 1: Define the Time Window and Query Parameters

The NICE CXone Reporting API uses ISO 8601 timestamps for date ranges. To query the last 24 hours, we calculate the start and end times dynamically.

The core endpoint is /api/v2/reporting/interactions/agents/summary. However, for detailed state history (login/logout/idle), the interactions/agents/details or specific queue reporting endpoints are often more granular. For “Agent State History,” the most reliable v2 endpoint is /api/v2/reporting/interactions/agents/details.

Required OAuth Scope: reporting:read

Key parameters:

  • startDate: ISO 8601 string (e.g., 2023-10-27T00:00:00.000Z).
  • endDate: ISO 8601 string.
  • groupBy: Determines how data is aggregated. For history, byInterval or byAgent is useful.
  • metrics: The specific metrics to retrieve. For state history, agentStateDuration or loginDuration are critical.
def get_time_range_iso() -> Dict[str, str]:
    """
    Calculates the start and end timestamps for the last 24 hours in ISO 8601 format.
    """
    end_time = datetime.utcnow()
    start_time = end_time - timedelta(hours=24)
    
    # Format as ISO 8601 with Zulu time
    start_str = start_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
    end_str = end_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
    
    return {
        "startDate": start_str,
        "endDate": end_str
    }

Step 2: Construct the API Request

We will call /api/v2/reporting/interactions/agents/details. This endpoint provides detailed interaction data including agent state transitions.

Endpoint: POST https://{domain}/api/v2/reporting/interactions/agents/details

Request Body Structure:

{
  "startDate": "2023-10-26T10:00:00.000Z",
  "endDate": "2023-10-27T10:00:00.000Z",
  "groupBy": [
    "byAgent",
    "byInterval"
  ],
  "metrics": [
    "agentStateDuration",
    "loginDuration",
    "wrapupDuration"
  ],
  "filters": [
    {
      "name": "agentId",
      "values": ["agent-uuid-here"]
    }
  ]
}

Note: If you omit the agentId filter, the API returns data for all agents, which can be massive. For a specific agent history, always filter by agentId.

class CXoneReportingClient:
    def __init__(self, auth_client: CXoneAuth):
        self.auth = auth_client
        self.base_url = auth_client.base_url
        self.session = requests.Session()

    def get_agent_state_history(self, agent_id: str, interval_minutes: int = 15) -> List[Dict[str, Any]]:
        """
        Queries the CXone Reporting API for agent state history over the last 24 hours.
        
        Args:
            agent_id: The UUID of the agent.
            interval_minutes: The granularity of the reporting interval (e.g., 15, 30, 60).
            
        Returns:
            A list of dictionaries containing agent state metrics.
        """
        token = self.auth.get_access_token()
        
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "Accept": "application/json"
        }

        time_range = get_time_range_iso()
        
        # The groupBy determines the shape of the response.
        # byInterval gives us time slices, byAgent gives us agent-level aggregation.
        # For history, we want to see how states changed over time intervals.
        payload = {
            "startDate": time_range["startDate"],
            "endDate": time_range["endDate"],
            "groupBy": [
                "byInterval",
                "byAgentState"  # Group by specific states like Available, Busy, etc.
            ],
            "metrics": [
                "agentStateDuration"  # How long the agent spent in each state
            ],
            "filters": [
                {
                    "name": "agentId",
                    "values": [agent_id]
                }
            ],
            "interval": f"{interval_minutes}m"  # Optional: define interval size
        }

        endpoint = f"{self.base_url}/reporting/interactions/agents/details"

        try:
            response = self.session.post(
                endpoint,
                json=payload,
                headers=headers,
                timeout=30
            )
            response.raise_for_status()
            
            data = response.json()
            return self._parse_response(data)

        except requests.exceptions.HTTPError as http_err:
            if response.status_code == 401:
                raise Exception("Unauthorized: Token is invalid or expired.") from http_err
            elif response.status_code == 403:
                raise Exception("Forbidden: Missing 'reporting:read' scope.") from http_err
            elif response.status_code == 429:
                raise Exception("Rate Limited: Too many requests. Implement backoff.") from http_err
            else:
                print(f"Error Response Body: {response.text}")
                raise Exception(f"HTTP Error: {http_err}") from http_err
        except requests.exceptions.RequestException as req_err:
            raise Exception(f"Network error: {req_err}") from req_err

    def _parse_response(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses the CXone API response into a flat list of state history records.
        """
        results = []
        
        # The response structure is typically:
        # {
        #   "reportData": {
        #     "reportRows": [ ... ]
        #   }
        # }
        
        report_data = data.get("reportData", {})
        rows = report_data.get("reportRows", [])
        
        for row in rows:
            # Extract dimensions
            agent_state = row.get("dimensions", {}).get("agentState", "Unknown")
            interval_start = row.get("dimensions", {}).get("intervalStart", "")
            interval_end = row.get("dimensions", {}).get("intervalEnd", "")
            
            # Extract metrics
            metrics = row.get("metrics", {})
            duration_ms = metrics.get("agentStateDuration", 0)
            
            results.append({
                "agentState": agent_state,
                "intervalStart": interval_start,
                "intervalEnd": interval_end,
                "durationMilliseconds": duration_ms,
                "durationSeconds": duration_ms / 1000 if duration_ms else 0
            })
            
        return results

Step 3: Processing Results and Handling Pagination

The CXone Reporting API does not always paginate in the traditional sense for small datasets, but for large date ranges or many agents, it may return a nextUri. While the agents/details endpoint often returns all data in one shot for single-agent queries, robust code should check for nextUri in the response metadata if the API version supports it.

For this specific endpoint, the response usually contains all rows in reportRows. However, if the data exceeds the limit, the API might truncate. Always check the reportData structure.

    def get_agent_state_history_with_pagination(self, agent_id: str, interval_minutes: int = 15) -> List[Dict[str, Any]]:
        """
        Enhanced version that handles potential pagination via nextUri if present.
        """
        all_results = []
        token = self.auth.get_access_token()
        
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "Accept": "application/json"
        }

        time_range = get_time_range_iso()
        
        payload = {
            "startDate": time_range["startDate"],
            "endDate": time_range["endDate"],
            "groupBy": ["byInterval", "byAgentState"],
            "metrics": ["agentStateDuration"],
            "filters": [{"name": "agentId", "values": [agent_id]}],
            "interval": f"{interval_minutes}m"
        }

        endpoint = f"{self.base_url}/reporting/interactions/agents/details"
        
        while endpoint:
            try:
                response = self.session.post(
                    endpoint,
                    json=payload,
                    headers=headers,
                    timeout=30
                )
                response.raise_for_status()
                
                data = response.json()
                
                # Parse current batch
                rows = data.get("reportData", {}).get("reportRows", [])
                for row in rows:
                    agent_state = row.get("dimensions", {}).get("agentState", "Unknown")
                    interval_start = row.get("dimensions", {}).get("intervalStart", "")
                    duration_ms = row.get("metrics", {}).get("agentStateDuration", 0)
                    
                    all_results.append({
                        "agentState": agent_state,
                        "intervalStart": interval_start,
                        "durationSeconds": duration_ms / 1000 if duration_ms else 0
                    })
                
                # Check for next page
                next_uri = data.get("nextUri")
                if next_uri:
                    # Note: For POST requests, pagination often requires re-posting to the nextUri
                    # with the same body, or the API returns a GET-able nextUri.
                    # CXone Reporting v2 typically uses POST for queries.
                    # If nextUri is present, we continue the loop.
                    endpoint = next_uri
                    # Important: If nextUri is a POST target, we keep using POST.
                    # Some implementations require switching to GET for subsequent pages.
                    # Check CXone docs for specific endpoint behavior.
                    # For safety, we will assume the same method unless specified otherwise.
                else:
                    endpoint = None

            except requests.exceptions.HTTPError as http_err:
                raise Exception(f"HTTP Error during pagination: {http_err}") from http_err

        return all_results

Complete Working Example

This script combines authentication, querying, and outputting the agent state history for the last 24 hours.

import requests
import json
from datetime import datetime, timedelta
from typing import Dict, List, Any

# --- Configuration ---
CXONE_DOMAIN = "your-domain.nicecxone.com"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
AGENT_UUID = "550e8400-e29b-41d4-a716-446655440000" # Replace with a valid Agent UUID

# --- Classes ---

class CXoneAuth:
    def __init__(self, domain: str, client_id: str, client_secret: str):
        self.domain = domain
        self.client_id = client_id
        self.client_secret = client_secret
        self.access_token = None

    def get_access_token(self) -> str:
        token_url = f"https://{self.domain}/api/v2/oauth/token"
        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(token_url, data=payload, headers=headers, timeout=10)
            response.raise_for_status()
            self.access_token = response.json().get("access_token")
            return self.access_token
        except requests.exceptions.HTTPError as e:
            raise Exception(f"Auth Failed: {e.response.text}") from e

class CXoneReporter:
    def __init__(self, auth: CXoneAuth):
        self.auth = auth
        self.base_url = f"https://{auth.domain}/api/v2"
        self.session = requests.Session()

    def get_last_24h_state_history(self, agent_id: str) -> List[Dict[str, Any]]:
        token = self.auth.get_access_token()
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json"
        }
        
        end_time = datetime.utcnow()
        start_time = end_time - timedelta(hours=24)
        
        payload = {
            "startDate": start_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
            "endDate": end_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
            "groupBy": ["byInterval", "byAgentState"],
            "metrics": ["agentStateDuration"],
            "filters": [{"name": "agentId", "values": [agent_id]}],
            "interval": "15m" # 15-minute intervals
        }
        
        endpoint = f"{self.base_url}/reporting/interactions/agents/details"
        
        try:
            response = self.session.post(endpoint, json=payload, headers=headers, timeout=30)
            response.raise_for_status()
            data = response.json()
            
            report_rows = data.get("reportData", {}).get("reportRows", [])
            
            history = []
            for row in report_rows:
                dims = row.get("dimensions", {})
                metrics = row.get("metrics", {})
                
                history.append({
                    "state": dims.get("agentState", "Unknown"),
                    "start": dims.get("intervalStart", ""),
                    "duration_sec": metrics.get("agentStateDuration", 0) / 1000.0
                })
            
            # Sort by time
            history.sort(key=lambda x: x["start"])
            return history
            
        except requests.exceptions.HTTPError as e:
            print(f"Error: {e}")
            print(f"Response: {e.response.text}")
            return []

# --- Main Execution ---

if __name__ == "__main__":
    try:
        auth = CXoneAuth(CXONE_DOMAIN, CLIENT_ID, CLIENT_SECRET)
        reporter = CXoneReporter(auth)
        
        print(f"Fetching last 24h state history for Agent: {AGENT_UUID}...")
        
        history = reporter.get_last_24h_state_history(AGENT_UUID)
        
        if not history:
            print("No data found. Ensure the agent has activity in the last 24 hours.")
        else:
            print(f"\n{'State':<15} | {'Interval Start':<25} | {'Duration (sec)':<15}")
            print("-" * 60)
            for entry in history:
                print(f"{entry['state']:<15} | {entry['start']:<25} | {entry['duration_sec']:<15.2f}")

    except Exception as e:
        print(f"Fatal Error: {e}")

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token is invalid, expired, or the Client ID/Secret is incorrect.
  • Fix: Verify your Client ID and Secret in the CXone Admin Console under Developers > OAuth. Ensure the token is fetched fresh before the API call.

Error: 403 Forbidden

  • Cause: The OAuth Client does not have the reporting:read scope.
  • Fix: Go to Developers > OAuth, edit your client, and ensure Reporting > Read is checked under Scopes. Save and regenerate the token.

Error: 400 Bad Request

  • Cause: Invalid ISO 8601 date format or invalid agentId.
  • Fix: Ensure startDate and endDate are in YYYY-MM-DDTHH:mm:ss.000Z format. Verify the agentId is a valid UUID from your CXone instance.

Error: Empty Response

  • Cause: The agent was not logged in or had no state changes in the last 24 hours.
  • Fix: Check the agent’s login history in the UI. If the agent was offline, agentStateDuration metrics may not exist for that period.

Official References