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

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

What You Will Build

  • A Python script that retrieves granular agent state transitions (Login, Logout, Ready, Not Ready, Wrap-Up) for specific agents over the previous 24 hours.
  • The solution uses the NICE CXone Reporting API v2 endpoint /api/v2/reporting/agents/states/query.
  • The tutorial covers Python 3.10+ with the requests library and standard JSON processing.

Prerequisites

OAuth Client Configuration

To access the Reporting API, you must have an OAuth 2.0 Client ID and Secret. The client must be assigned the Reporting application scope.

  • Grant Type: Client Credentials (recommended for server-side scripts) or Authorization Code (if acting on behalf of a user).
  • Required Scopes: reporting:read is the minimum requirement. If you need to filter by user ID rather than name, ensure your token has sufficient permissions to resolve user entities.

Environment Setup

  • Python Version: 3.10 or higher.
  • Dependencies:
    pip install requests python-dotenv
    
  • API Version: NICE CXone API v2.
  • Environment Variables: You will need to store your CXONE_CLIENT_ID, CXONE_CLIENT_SECRET, and CXONE_SUBDOMAIN (e.g., dev123 or prod456).

Authentication Setup

NICE CXone uses standard OAuth 2.0 Client Credentials flow for server-to-server communication. The token endpoint is located at https://platform.dev[env].niceincontact.com/oauth/token.

Token Acquisition Code

Create a file named .env in your project root:

CXONE_CLIENT_ID=your_client_id_here
CXONE_CLIENT_SECRET=your_client_secret_here
CXONE_SUBDOMAIN=dev123

Create auth.py to handle token retrieval and caching:

import os
import requests
from datetime import datetime, timedelta, timezone

class CXoneAuth:
    def __init__(self, client_id: str, client_secret: str, subdomain: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.subdomain = subdomain
        self.token_url = f"https://platform.{subdomain}.niceincontact.com/oauth/token"
        self.access_token = None
        self.token_expiry = None

    def get_token(self) -> str:
        """
        Retrieves an OAuth2 access token.
        Implements basic caching to avoid unnecessary token refreshes.
        """
        # Check if we have a valid token
        if self.access_token and self.token_expiry and datetime.now(timezone.utc) < self.token_expiry:
            return self.access_token

        # Request new token
        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }
        data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret
        }

        try:
            response = requests.post(self.token_url, headers=headers, data=data)
            response.raise_for_status()
            token_data = response.json()
            
            self.access_token = token_data["access_token"]
            expires_in = token_data.get("expires_in", 3600)
            # Set expiry slightly before actual expiry to ensure validity
            self.token_expiry = datetime.now(timezone.utc) + timedelta(seconds=expires_in - 30)
            
            return self.access_token
        except requests.exceptions.HTTPError as e:
            raise Exception(f"Failed to obtain OAuth token: {e.response.text}") from e
        except requests.exceptions.RequestException as e:
            raise Exception(f"Network error during token request: {str(e)}") from e

# Usage Example
# auth = CXoneAuth(
#     os.getenv("CXONE_CLIENT_ID"),
#     os.getenv("CXONE_CLIENT_SECRET"),
#     os.getenv("CXONE_SUBDOMAIN")
# )
# token = auth.get_token()

Implementation

Step 1: Constructing the Reporting Query Payload

The NICE CXone Reporting API uses a complex JSON payload to define time ranges, filters, and grouping. For agent state history, the critical component is the timeRange and the filters.

Unlike standard REST GET requests, this endpoint requires a POST request with a JSON body. The API does not return a simple list of events; it returns aggregated data unless specific grouping parameters are set. To get “history,” we must group by agent and state, and often by time if we want granularity.

Key Payload Parameters:

  • timeRange: Defines the start and end of the query window. We will calculate this dynamically for the last 24 hours.
  • filters: Restricts data to specific agents or skills.
  • groupings: Determines how the data is bucketed. To see state changes, we typically group by agent and state.
  • metrics: While state history is often derived from time-in-state metrics, the agents/states/query endpoint specifically returns state transition records when configured correctly.

Step 2: Building the Request Function

We will create a function that accepts a list of agent IDs (or names) and returns the state history.

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

class CXoneReportingClient:
    def __init__(self, subdomain: str, access_token: str):
        self.subdomain = subdomain
        self.access_token = access_token
        self.base_url = f"https://platform.{subdomain}.niceincontact.com"
        self.reporting_endpoint = "/api/v2/reporting/agents/states/query"

    def get_agent_state_history(
        self, 
        agent_ids: List[str], 
        hours_back: int = 24
    ) -> Dict[str, Any]:
        """
        Queries agent state history for the specified agents over the last N hours.
        
        Args:
            agent_ids: List of Agent IDs (UUIDs or Names). 
                      Note: IDs are preferred for accuracy.
            hours_back: Number of hours to look back (default 24).
            
        Returns:
            Parsed JSON response from the API.
        """
        
        # 1. Calculate Time Range
        end_time = datetime.now(timezone.utc)
        start_time = end_time - timedelta(hours=hours_back)
        
        # Format as ISO 8601 strings required by the API
        start_time_str = start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
        end_time_str = end_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"

        # 2. Construct the Payload
        payload = {
            "timeRange": {
                "from": start_time_str,
                "to": end_time_str
            },
            "filters": {
                "agentIds": agent_ids
            },
            "groupings": [
                "agent",
                "state"
            ],
            # Optional: Add time grouping if you want hourly breakdowns
            # "timeGrouping": "HOURLY", 
            "metrics": [
                "agent.state.timeInState"
            ]
        }

        # 3. Prepare Headers
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.access_token}",
            "Accept": "application/json"
        }

        # 4. Execute Request
        url = f"{self.base_url}{self.reporting_endpoint}"
        
        try:
            response = requests.post(url, headers=headers, json=payload)
            
            # Handle Rate Limiting (429)
            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", 5))
                print(f"Rate limited. Waiting {retry_after} seconds...")
                import time
                time.sleep(retry_after)
                # Retry once
                response = requests.post(url, headers=headers, json=payload)
                response.raise_for_status()
            
            response.raise_for_status()
            return response.json()
            
        except requests.exceptions.HTTPError as e:
            print(f"HTTP Error: {e.response.status_code}")
            print(f"Response Body: {e.response.text}")
            raise
        except requests.exceptions.RequestException as e:
            print(f"Request failed: {e}")
            raise

Step 3: Processing the Response

The NICE CXone Reporting API response structure is nested. The resultSets array contains the actual data. Each result set corresponds to a metric requested. Since we requested agent.state.timeInState, the data will be aggregated time spent in each state, not a chronological log of every click.

Important Distinction: The /agents/states/query endpoint provides aggregated state data. If you require a chronological log of every state change (e.g., “Logged in at 10:00, Ready at 10:05”), you must use the Activity Log API or parse the timeInState metrics against known shift schedules. For this tutorial, we will parse the aggregated state times, which is the standard method for reporting “how much time an agent spent in Ready vs. Not Ready.”

def parse_state_results(response_data: Dict[str, Any]) -> List[Dict[str, Any]]:
    """
    Parses the raw API response into a flat list of agent state summaries.
    """
    results = []
    
    # The API returns a list of resultSets. We look for the one matching our metric.
    for result_set in response_data.get("resultSets", []):
        metric_name = result_set.get("metricName")
        
        if metric_name == "agent.state.timeInState":
            rows = result_set.get("rows", [])
            for row in rows:
                # Extract values based on groupings: [agent, state]
                # The 'values' array corresponds to the metrics array in the request
                agent_name = row.get("groupingValues", [None])[0]
                state_name = row.get("groupingValues", [None])[1]
                
                # Get the time value (in seconds)
                time_in_state_seconds = row.get("values", [0])[0]
                
                if agent_name and state_name:
                    results.append({
                        "agent": agent_name,
                        "state": state_name,
                        "time_in_state_seconds": time_in_state_seconds,
                        "time_in_state_hours": round(time_in_state_seconds / 3600, 2)
                    })
                    
    return results

Complete Working Example

Combine the authentication and reporting logic into a single executable script.

import os
import sys
from dotenv import load_dotenv
from auth import CXoneAuth
from cxone_reporting import CXoneReportingClient, parse_state_results

def main():
    # 1. Load Environment Variables
    load_dotenv()
    
    client_id = os.getenv("CXONE_CLIENT_ID")
    client_secret = os.getenv("CXONE_CLIENT_SECRET")
    subdomain = os.getenv("CXONE_SUBDOMAIN")
    
    if not all([client_id, client_secret, subdomain]):
        print("Error: Missing environment variables. Check .env file.")
        sys.exit(1)

    # 2. Initialize Auth
    try:
        auth = CXoneAuth(client_id, client_secret, subdomain)
        token = auth.get_token()
        print("OAuth Token acquired successfully.")
    except Exception as e:
        print(f"Authentication failed: {e}")
        sys.exit(1)

    # 3. Initialize Reporting Client
    reporting_client = CXoneReportingClient(subdomain, token)

    # 4. Define Agents to Query
    # Replace these with actual Agent IDs or Names from your CXone instance
    # Using Names is easier for testing, but IDs are more stable.
    agent_ids = [
        "John Doe", 
        "Jane Smith"
    ]

    print(f"Querying state history for {len(agent_ids)} agents for the last 24 hours...")

    try:
        # 5. Execute Query
        raw_response = reporting_client.get_agent_state_history(agent_ids, hours_back=24)
        
        # 6. Parse Results
        state_summary = parse_state_results(raw_response)
        
        if not state_summary:
            print("No state data found for the specified agents in the last 24 hours.")
            return

        # 7. Display Results
        print("\n--- Agent State Summary (Last 24 Hours) ---")
        print(f"{'Agent':<20} | {'State':<15} | {'Hours':<10}")
        print("-" * 50)
        
        for record in state_summary:
            print(f"{record['agent']:<20} | {record['state']:<15} | {record['time_in_state_hours']:<10.2f}")
            
        # 8. Save to JSON for further analysis
        with open("agent_state_report.json", "w") as f:
            json.dump(state_summary, f, indent=2)
        print("\nReport saved to agent_state_report.json")

    except Exception as e:
        print(f"Reporting query failed: {e}")

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token is expired, invalid, or missing scopes.
  • Fix: Ensure your CXoneAuth class is refreshing the token if expires_in has passed. Verify that the Client ID has the reporting:read scope assigned in the CXone Admin UI under Developers > OAuth Clients.

Error: 403 Forbidden

  • Cause: The OAuth client does not have permission to access reporting data, or the user associated with the token (if using Authorization Code flow) lacks reporting privileges.
  • Fix: Check the Application assignment for your OAuth client. It must be assigned to the Reporting application. If using User Impersonation, ensure the user has a role with “View Reports” permissions.

Error: 429 Too Many Requests

  • Cause: The Reporting API has strict rate limits (typically around 10-20 requests per minute depending on your contract).
  • Fix: The code above includes a basic retry mechanism. For production, implement exponential backoff. Do not loop through agents one by one; instead, pass a list of up to 50 agent IDs in a single request payload to minimize API calls.

Error: Empty resultSets

  • Cause: No data exists for the specified time range or agents.
  • Fix:
    1. Verify the timeRange is in the past. The API often does not return data for the current minute due to processing latency.
    2. Ensure the agentIds are correct. If using names, ensure they match exactly (case-sensitive).
    3. Check if the agents actually logged in during the requested window.

Error: 500 Internal Server Error

  • Cause: Malformed JSON payload or unsupported metric/grouping combination.
  • Fix: Validate the JSON payload against the NICE CXone API Swagger definition. Ensure groupings match the expected dimensions for the requested metrics. For agent.state.timeInState, grouping by agent and state is mandatory.

Official References