NICE CXone Reporting API (v2) — Querying Agent State History for the Last 24 Hours
What You Will Build
- A Python script that retrieves granular agent state changes (Login, Logout, Ready, Not Ready) for a specific agent or group over the previous 24 hours.
- This tutorial uses the NICE CXone Reporting API (v2) endpoint
/reporting/v2/agents/state-history. - The implementation covers Python 3.9+ using the
requestslibrary, with full OAuth 2.0 Client Credentials flow handling.
Prerequisites
Before writing code, ensure you have the following configured in your NICE CXone environment:
- OAuth Client Application: You need an OAuth Client with the
client_credentialsgrant type. - Required Scopes: The client must have the
reporting:viewscope. Without this, the API will return a 403 Forbidden error. - Environment URL: Your specific CXone environment URL (e.g.,
https://api-us-1.cxone.nice.comorhttps://api-eu-1.cxone.nice.com). - Python Environment: Python 3.9 or higher installed.
- Dependencies: Install the
requestslibrary if not already present.
pip install requests
Authentication Setup
NICE CXone APIs use OAuth 2.0 for authentication. For server-to-server integrations like reporting scripts, the Client Credentials Flow is the standard. This flow exchanges your Client ID and Client Secret for a short-lived access token.
The token endpoint is always https://[YOUR_ENVIRONMENT].cxone.nice.com/oauth/token.
Step 1: Retrieve the Access Token
The following function handles the token request. It includes error handling for common failures such as invalid credentials (401) or network timeouts.
import requests
import time
from datetime import datetime, timezone, timedelta
# Configuration - Replace these with your actual values
CXONE_ENV = "api-us-1.cxone.nice.com" # Example: api-eu-1.cxone.nice.com
CLIENT_ID = "your_client_id_here"
CLIENT_SECRET = "your_client_secret_here"
BASE_URL = f"https://{CXONE_ENV}"
def get_access_token() -> str:
"""
Retrieves an OAuth 2.0 access token using the Client Credentials flow.
Returns:
str: The access token string.
Raises:
requests.exceptions.HTTPError: If authentication fails.
requests.exceptions.RequestException: If network errors occur.
"""
token_url = f"{BASE_URL}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": 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()
access_token = token_data.get("access_token")
if not access_token:
raise ValueError("Access token not found in response")
return access_token
except requests.exceptions.HTTPError as http_err:
if response.status_code == 401:
print("Authentication Failed: Check your Client ID and Secret.")
elif response.status_code == 403:
print("Forbidden: Check your OAuth Scopes (requires reporting:view).")
else:
print(f"HTTP Error: {http_err}")
raise
except requests.exceptions.RequestException as req_err:
print(f"Network Error: {req_err}")
raise
# Cache token to avoid unnecessary refreshes within the token's lifetime
_cached_token = None
_token_expiry = 0
def get_valid_token() -> str:
global _cached_token, _token_expiry
# Check if cached token is still valid (refresh 1 minute before expiry)
if _cached_token and time.time() < _token_expiry - 60:
return _cached_token
print("Fetching new access token...")
token = get_access_token()
# Note: The CXone token response does not always include an explicit 'expires_in'
# in all environments, but typically it is 3600 seconds (1 hour).
# We set a conservative cache time of 55 minutes.
_cached_token = token
_token_expiry = time.time() + (55 * 60)
return _cached_token
Implementation
Step 1: Define the Query Parameters
The Agent State History API requires specific date range parameters. The endpoint expects ISO 8601 formatted timestamps. To get the last 24 hours, we calculate the start and end times dynamically.
Key parameters:
start: The beginning of the time window (ISO 8601).end: The end of the time window (ISO 8601).agentId: (Optional) Filter by a specific agent’s UUID. If omitted, it returns data for all agents in the scope of the OAuth client.limit: Maximum number of records returned per page (default 100, max 1000).offset: For pagination.
def get_last_24_hours_range():
"""
Calculates the start and end timestamps for the last 24 hours.
Returns:
tuple: (start_time_iso, end_time_iso)
"""
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(hours=24)
# Format as ISO 8601 with 'Z' suffix for UTC
start_iso = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
end_iso = end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
return start_iso, end_iso
# Example Usage
start, end = get_last_24_hours_range()
print(f"Querying from {start} to {end}")
Step 2: Construct the API Request
The core API call uses the GET method on /reporting/v2/agents/state-history. This endpoint returns a list of state transitions. Each record contains the timestamp, agentId, previousState, newState, and skill (if applicable).
We must handle pagination because the default limit is 100 records. If an agent is active, they may generate many state changes (e.g., Ready → Not Ready → Ready) in 24 hours.
def fetch_agent_state_history(agent_id: str = None, limit: int = 1000):
"""
Fetches agent state history for the last 24 hours with pagination support.
Args:
agent_id (str, optional): UUID of the agent. If None, fetches for all agents.
limit (int): Max records per page (max 1000).
Returns:
list: A list of all state history records.
"""
token = get_valid_token()
api_url = f"{BASE_URL}/reporting/v2/agents/state-history"
start, end = get_last_24_hours_range()
all_records = []
offset = 0
# Headers required for CXone API
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json"
}
# Base parameters
params = {
"start": start,
"end": end,
"limit": limit
}
if agent_id:
params["agentId"] = agent_id
print(f"Starting pagination fetch for agent: {agent_id or 'All'}")
while True:
try:
response = requests.get(api_url, headers=headers, params=params, timeout=30)
response.raise_for_status()
data = response.json()
# The API returns a list of records directly in the root or under a 'records' key
# depending on the exact sub-endpoint version. For v2/agents/state-history,
# it typically returns a list. However, some CXone reporting endpoints wrap in 'data'.
# We check for the list type to be safe.
records = data if isinstance(data, list) else data.get("records", [])
if not records:
print("No more records found.")
break
all_records.extend(records)
print(f"Fetched {len(records)} records. Total so far: {len(all_records)}")
# Check if we have fewer records than the limit.
# If yes, we have reached the end of the dataset.
if len(records) < limit:
print("Reached end of dataset.")
break
# Prepare for next page
offset += limit
params["offset"] = offset
# Small delay to respect rate limits (good practice for reporting APIs)
time.sleep(0.5)
except requests.exceptions.HTTPError as e:
if response.status_code == 429:
print("Rate Limited (429). Waiting 10 seconds before retry...")
time.sleep(10)
continue
else:
print(f"HTTP Error {response.status_code}: {response.text}")
break
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
break
return all_records
Step 3: Processing and Formatting Results
The raw JSON response contains technical state codes (e.g., LOGIN, READY, NOTREADY). For a usable report, we should map these to human-readable labels and calculate durations if possible. However, the state history API provides events, not intervals. To get durations, you would typically pair this with the agent-activity API or calculate the difference between consecutive events for the same agent.
For this tutorial, we will focus on cleaning the event data into a structured format.
STATE_LABELS = {
"LOGIN": "Logged In",
"LOGOUT": "Logged Out",
"READY": "Ready",
"NOTREADY": "Not Ready",
"PAUSE": "Paused",
"RESUME": "Resumed",
"AVAILABLE": "Available",
"UNAVAILABLE": "Unavailable"
}
def process_state_events(events: list) -> list:
"""
Transforms raw API events into a more readable format.
Args:
events (list): Raw list of state history events from API.
Returns:
list: List of dictionaries with processed data.
"""
processed = []
for event in events:
# Extract key fields
agent_id = event.get("agentId", "Unknown")
timestamp_str = event.get("timestamp", "Unknown")
new_state_code = event.get("newState", "Unknown")
prev_state_code = event.get("previousState", "")
# Map codes to labels
new_state_label = STATE_LABELS.get(new_state_code, new_state_code)
prev_state_label = STATE_LABELS.get(prev_state_code, prev_state_code) if prev_state_code else "N/A"
# Parse timestamp for sorting or further calculation
try:
dt_obj = datetime.strptime(timestamp_str, "%Y-%m-%dT%H:%M:%SZ")
dt_obj = dt_obj.replace(tzinfo=timezone.utc)
except ValueError:
dt_obj = None
processed.append({
"agent_id": agent_id,
"timestamp": dt_obj,
"timestamp_str": timestamp_str,
"previous_state": prev_state_label,
"new_state": new_state_label,
"raw_code": new_state_code
})
# Sort by timestamp ascending
processed.sort(key=lambda x: x["timestamp"] if x["timestamp"] else datetime.min.replace(tzinfo=timezone.utc))
return processed
Complete Working Example
Below is the complete, runnable Python script. Copy this into a file named cxone_agent_state_report.py. Ensure you update the CLIENT_ID, CLIENT_SECRET, and CXONE_ENV variables at the top.
import requests
import time
import json
from datetime import datetime, timezone, timedelta
# ==========================================
# CONFIGURATION
# ==========================================
CXONE_ENV = "api-us-1.cxone.nice.com" # Replace with your environment
CLIENT_ID = "your_client_id_here" # Replace with your Client ID
CLIENT_SECRET = "your_client_secret_here" # Replace with your Client Secret
BASE_URL = f"https://{CXONE_ENV}"
# Optional: Specify an agent UUID to filter results.
# Set to None to fetch for all agents in the tenant.
TARGET_AGENT_ID = None
# ==========================================
# AUTHENTICATION
# ==========================================
_cached_token = None
_token_expiry = 0
def get_access_token() -> str:
token_url = f"{BASE_URL}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": 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()
return response.json().get("access_token")
except requests.exceptions.HTTPError as e:
print(f"Auth Error: {e.response.status_code} - {e.response.text}")
raise
except Exception as e:
print(f"Auth Request Failed: {e}")
raise
def get_valid_token() -> str:
global _cached_token, _token_expiry
if _cached_token and time.time() < _token_expiry - 60:
return _cached_token
print("Refreshing access token...")
token = get_access_token()
_cached_token = token
_token_expiry = time.time() + (55 * 60) # 55 min cache
return _cached_token
# ==========================================
# API LOGIC
# ==========================================
def get_last_24_hours_range():
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(hours=24)
return start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
def fetch_agent_state_history(agent_id: str = None, limit: int = 1000) -> list:
token = get_valid_token()
api_url = f"{BASE_URL}/reporting/v2/agents/state-history"
start, end = get_last_24_hours_range()
all_records = []
offset = 0
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json"
}
params = {
"start": start,
"end": end,
"limit": limit
}
if agent_id:
params["agentId"] = agent_id
print(f"Querying Agent State History from {start} to {end}")
if agent_id:
print(f"Filtering for Agent ID: {agent_id}")
else:
print("Fetching for all agents.")
while True:
try:
response = requests.get(api_url, headers=headers, params=params, timeout=30)
if response.status_code == 429:
print("Rate Limited. Waiting 10s...")
time.sleep(10)
continue
response.raise_for_status()
data = response.json()
# Handle different response structures
records = data if isinstance(data, list) else data.get("records", [])
if not records:
break
all_records.extend(records)
print(f"Retrieved {len(records)} records. Total: {len(all_records)}")
if len(records) < limit:
break
offset += limit
params["offset"] = offset
time.sleep(0.2) # Politeness delay
except requests.exceptions.RequestException as e:
print(f"Error fetching data: {e}")
break
return all_records
# ==========================================
# PROCESSING
# ==========================================
STATE_LABELS = {
"LOGIN": "Logged In",
"LOGOUT": "Logged Out",
"READY": "Ready",
"NOTREADY": "Not Ready",
"PAUSE": "Paused",
"RESUME": "Resumed",
"AVAILABLE": "Available",
"UNAVAILABLE": "Unavailable"
}
def format_output(events: list) -> list:
processed = []
for event in events:
new_state_code = event.get("newState", "UNKNOWN")
prev_state_code = event.get("previousState", "")
processed.append({
"Agent ID": event.get("agentId", "N/A"),
"Timestamp": event.get("timestamp", "N/A"),
"Previous State": STATE_LABELS.get(prev_state_code, prev_state_code) if prev_state_code else "N/A",
"New State": STATE_LABELS.get(new_state_code, new_state_code),
"Skill": event.get("skill", "N/A")
})
return processed
# ==========================================
# MAIN EXECUTION
# ==========================================
if __name__ == "__main__":
try:
# 1. Fetch Raw Data
raw_events = fetch_agent_state_history(agent_id=TARGET_AGENT_ID)
if not raw_events:
print("No state history events found for the specified period.")
else:
# 2. Process Data
formatted_data = format_output(raw_events)
# 3. Output Results
print("\n--- Agent State History Report (Last 24 Hours) ---")
print(f"{'Agent ID':<36} | {'Timestamp':<20} | {'Previous State':<15} | {'New State':<12} | {'Skill'}")
print("-" * 100)
for row in formatted_data:
print(f"{row['Agent ID']:<36} | {row['Timestamp']:<20} | {row['Previous State']:<15} | {row['New State']:<12} | {row['Skill']}")
print("-" * 100)
print(f"Total Events: {len(formatted_data)}")
# Optional: Save to JSON
with open("agent_state_history.json", "w") as f:
json.dump(formatted_data, f, indent=2)
print("Results saved to agent_state_history.json")
except Exception as e:
print(f"Critical Error: {e}")
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The Client ID or Client Secret is incorrect, or the token has expired.
- Fix: Verify the credentials in the NICE CXone Admin Console under Settings > OAuth > Clients. Ensure the client is active. In the code, check that
get_access_token()is being called before the API request.
Error: 403 Forbidden
- Cause: The OAuth Client does not have the
reporting:viewscope. - Fix: Go to Settings > OAuth > Clients, edit your client, and add
reporting:viewto the Scopes list. Save and regenerate credentials if necessary.
Error: 404 Not Found
- Cause: Incorrect Environment URL.
- Fix: Ensure you are using the correct region endpoint.
- US:
api-us-1.cxone.nice.com - EU:
api-eu-1.cxone.nice.com - AU:
api-au-1.cxone.nice.com
- US:
Error: Empty Response
- Cause: No state changes occurred in the last 24 hours, or the
agentIdfilter is targeting an inactive agent. - Fix: Remove the
agentIdparameter to fetch data for all agents. Verify that agents are actually logged in and changing states during the queried window.
Error: 429 Too Many Requests
- Cause: You are hitting the rate limit (typically 10-20 requests per second for reporting APIs).
- Fix: The provided code includes a
time.sleep(0.2)and a 429 retry handler. If you are fetching for many agents, consider increasing the sleep interval or batching requests less frequently.