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
requestslibrary 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:readis 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, andCXONE_SUBDOMAIN(e.g.,dev123orprod456).
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 byagentandstate.metrics: While state history is often derived from time-in-state metrics, theagents/states/queryendpoint 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
CXoneAuthclass is refreshing the token ifexpires_inhas passed. Verify that the Client ID has thereporting:readscope 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:
- Verify the
timeRangeis in the past. The API often does not return data for the current minute due to processing latency. - Ensure the
agentIdsare correct. If using names, ensure they match exactly (case-sensitive). - Check if the agents actually logged in during the requested window.
- Verify the
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
groupingsmatch the expected dimensions for the requestedmetrics. Foragent.state.timeInState, grouping byagentandstateis mandatory.