CXone Reporting API (v2) — How to Query Agent State History for the Last 24 Hours
What You Will Build
- A script that queries the NICE CXone Reporting API to retrieve the state history (e.g., Ready, Not Ready, Wrap-up) for a specific agent over the previous 24 hours.
- This implementation uses the
GET /api/v2/reporting/agents/statehistoryendpoint from the CXone API. - The programming language covered is Python 3.10+ using the
requestslibrary for HTTP interactions.
Prerequisites
To execute this code, you must have the following resources and configurations:
- NICE CXone Tenant Access: You need a user account with API access enabled.
- API Credentials: You must have an API Key (
api_key) and API Secret (api_secret) generated from the CXone Administration console. Alternatively, you can use OAuth 2.0 Client Credentials, but API Key/Secret is simpler for server-to-server scripts. - Required Scope: The user or API key must have the
Reportingscope enabled. Specifically, you need read access to reporting data. - Python Environment: Python 3.10 or higher installed.
- External Dependencies: The
requestslibrary. Install it via pip:
pip install requests
Authentication Setup
NICE CXone supports two primary authentication methods: OAuth 2.0 and API Key/Secret. For reporting scripts that run on a schedule or server-side, API Key/Secret is often preferred because it avoids the overhead of token refresh cycles for simple read operations. However, the Reporting API v2 strictly requires a valid Authorization header.
Below is the setup for generating a bearer token using the OAuth 2.0 Client Credentials flow, which is the most robust method for production integrations. If you are using API Keys, you would typically pass them in the header as Authorization: Basic <base64(api_key:api_secret)> or include them in the request body depending on the specific endpoint version, but CXone v2 Reporting generally expects a Bearer token derived from the OAuth flow or an API Key that maps to a token.
Note: Many CXone tenants allow direct API Key usage in the header for v1, but v2 reporting endpoints often enforce OAuth. The code below uses the standard OAuth token endpoint.
import requests
import base64
import time
from typing import Optional
class CXoneAuth:
"""
Handles OAuth 2.0 Token acquisition for NICE CXone.
"""
def __init__(self, api_key: str, api_secret: str, tenant_url: str):
self.api_key = api_key
self.api_secret = api_secret
# Ensure tenant_url ends with a slash for concatenation safety
self.tenant_url = tenant_url.rstrip('/') + '/'
self.token_endpoint = f"{self.tenant_url}api/v2/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0
def get_token(self) -> str:
"""
Retrieves an OAuth access token.
Returns the token string. Raises an exception if authentication fails.
"""
# Check if we have a valid cached token
if self.access_token and time.time() < self.token_expiry:
return self.access_token
# Prepare credentials
credentials = f"{self.api_key}:{self.api_secret}"
encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
headers = {
"Authorization": f"Basic {encoded_credentials}",
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"scope": "reporting"
}
try:
response = requests.post(
self.token_endpoint,
headers=headers,
data=data,
timeout=10
)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("Authentication failed: Invalid API Key or Secret.") from e
elif response.status_code == 403:
raise Exception("Authentication failed: API Key does not have 'reporting' scope.") from e
else:
raise Exception(f"Authentication request failed with status {response.status_code}: {response.text}") from e
except requests.exceptions.RequestException as e:
raise Exception(f"Network error during authentication: {e}") from e
token_data = response.json()
self.access_token = token_data.get("access_token")
# Tokens usually expire in 3600 seconds (1 hour). We subtract 60s for safety.
self.token_expiry = time.time() + (token_data.get("expires_in", 3600) - 60)
return self.access_token
Implementation
Step 1: Constructing the Query Parameters
The CXone Reporting API uses ISO 8601 date formats for time ranges. To query the last 24 hours, we must calculate the start and end timestamps dynamically. The from and to parameters are inclusive.
We also need the Agent ID. In CXone, this is typically the numeric ID associated with the user, not the email or name. If you do not have the ID, you must first query the User Management API (GET /api/v2/users) to map the name/email to the ID. For this tutorial, we assume the Agent ID is known.
from datetime import datetime, timedelta, timezone
def get_last_24_hours_range() -> tuple[str, str]:
"""
Calculates the ISO 8601 start and end timestamps for the last 24 hours.
CXone API expects UTC time.
"""
now = datetime.now(timezone.utc)
start_time = now - timedelta(hours=24)
# Format as ISO 8601 with timezone offset
# Example: 2023-10-27T14:30:00+00:00
start_iso = start_time.isoformat()
end_iso = now.isoformat()
return start_iso, end_iso
# Example usage
start_ts, end_ts = get_last_24_hours_range()
print(f"Querying from: {start_ts}")
print(f"Querying to: {end_ts}")
Step 2: Core Logic — Executing the State History Query
The endpoint GET /api/v2/reporting/agents/statehistory returns a list of state transitions. It supports pagination via the pageSize and pageNumber parameters, or cursor-based pagination depending on the specific tenant configuration. We will implement a loop to handle pagination to ensure we capture all states for the 24-hour period.
Required OAuth Scope: reporting
Endpoint: GET /api/v2/reporting/agents/statehistory
Parameters:
agentId(Required): The numeric ID of the agent.from(Required): Start timestamp (ISO 8601).to(Required): End timestamp (ISO 8601).pageSize(Optional): Number of records per page. Default is often 200. Max is usually 1000.pageNumber(Optional): Page number (1-based).
import json
def fetch_agent_state_history(
auth: CXoneAuth,
tenant_url: str,
agent_id: int,
start_time: str,
end_time: str,
page_size: int = 500
) -> list[dict]:
"""
Fetches all agent state history records for the given time range.
Handles pagination automatically.
"""
base_url = f"{tenant_url.rstrip('/')}/api/v2/reporting/agents/statehistory"
all_records = []
page_number = 1
has_more_pages = True
while has_more_pages:
params = {
"agentId": agent_id,
"from": start_time,
"to": end_time,
"pageSize": page_size,
"pageNumber": page_number
}
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Accept": "application/json"
}
try:
response = requests.get(
base_url,
headers=headers,
params=params,
timeout=30
)
# 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...")
time.sleep(retry_after)
continue # Retry the same page
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
# Token might have expired, try refreshing
auth.access_token = None
continue
elif response.status_code == 404:
raise Exception(f"Agent ID {agent_id} not found or no data available.") from e
else:
raise Exception(f"API Error: {response.status_code} - {response.text}") from e
except requests.exceptions.RequestException as e:
raise Exception(f"Network error: {e}") from e
data = response.json()
# CXone v2 Reporting usually returns a list directly or an object with 'items'
# The specific structure for statehistory is often a list of objects.
# Let's normalize the response.
records = data if isinstance(data, list) else data.get("items", [])
if not records:
has_more_pages = False
break
all_records.extend(records)
# Check if there are more pages
# CXone often returns totalResults or a link for next page.
# If the number of records returned is less than page_size, we are at the end.
if len(records) < page_size:
has_more_pages = False
else:
page_number += 1
return all_records
Step 3: Processing Results
The raw JSON response from CXone contains technical fields like stateId, code, description, and duration. To make this data useful, we should map the state IDs to human-readable labels and calculate the total time spent in each state.
Common State IDs in CXone:
1: Ready (Available)2: Not Ready (Break, Meeting, etc.)3: Wrap-up4: In Call (Active Interaction)
Note: State IDs can vary by tenant configuration. It is best practice to fetch the stateCode or description field if available.
def analyze_state_history(records: list[dict]) -> dict:
"""
Processes the raw state history records to provide a summary.
"""
summary = {
"total_records": len(records),
"state_durations": {}, # State ID -> Total seconds
"transitions": []
}
for record in records:
state_id = record.get("stateId")
duration = record.get("duration", 0) # Duration is in seconds
start_time = record.get("startTime")
end_time = record.get("endTime")
description = record.get("description", "Unknown")
if state_id:
if state_id not in summary["state_durations"]:
summary["state_durations"][state_id] = 0
summary["state_durations"][state_id] += duration
summary["transitions"].append({
"state_id": state_id,
"description": description,
"duration_sec": duration,
"start": start_time,
"end": end_time
})
return summary
def print_summary(summary: dict):
"""
Prints a formatted summary of the agent's activity.
"""
print(f"\n--- Agent State Summary ---")
print(f"Total State Changes: {summary['total_records']}")
print("\nTime Spent by State:")
# Sort by duration descending
sorted_states = sorted(summary['state_durations'].items(), key=lambda x: x[1], reverse=True)
for state_id, total_seconds in sorted_states:
minutes = total_seconds / 60
hours = total_seconds / 3600
print(f" State ID {state_id}: {hours:.2f} hours ({minutes:.1f} minutes)")
print("\nDetailed Transitions (Last 5):")
for t in summary['transitions'][-5:]:
print(f" [{t['start']}] -> State {t['state_id']} ({t['description']}) for {t['duration_sec']}s")
Complete Working Example
Below is the complete, copy-pasteable script. Replace the API_KEY, API_SECRET, TENANT_URL, and AGENT_ID variables with your actual values.
#!/usr/bin/env python3
"""
CXone Agent State History Reporter
Queries the last 24 hours of state history for a specific agent.
"""
import requests
import base64
import time
import json
from datetime import datetime, timedelta, timezone
from typing import Optional, List, Dict
# --- Configuration ---
API_KEY = "YOUR_API_KEY_HERE"
API_SECRET = "YOUR_API_SECRET_HERE"
TENANT_URL = "https://your-tenant.niceincontact.com" # Replace with your tenant URL
AGENT_ID = 123456 # Replace with the numeric Agent ID
# --- Authentication Class ---
class CXoneAuth:
def __init__(self, api_key: str, api_secret: str, tenant_url: str):
self.api_key = api_key
self.api_secret = api_secret
self.tenant_url = tenant_url.rstrip('/') + '/'
self.token_endpoint = f"{self.tenant_url}api/v2/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0
def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry:
return self.access_token
credentials = f"{self.api_key}:{self.api_secret}"
encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
headers = {
"Authorization": f"Basic {encoded_credentials}",
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"scope": "reporting"
}
try:
response = requests.post(
self.token_endpoint,
headers=headers,
data=data,
timeout=10
)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("Authentication failed: Invalid API Key or Secret.") from e
elif response.status_code == 403:
raise Exception("Authentication failed: API Key does not have 'reporting' scope.") from e
else:
raise Exception(f"Authentication request failed with status {response.status_code}: {response.text}") from e
except requests.exceptions.RequestException as e:
raise Exception(f"Network error during authentication: {e}") from e
token_data = response.json()
self.access_token = token_data.get("access_token")
self.token_expiry = time.time() + (token_data.get("expires_in", 3600) - 60)
return self.access_token
# --- Helper Functions ---
def get_last_24_hours_range() -> tuple[str, str]:
now = datetime.now(timezone.utc)
start_time = now - timedelta(hours=24)
return start_time.isoformat(), now.isoformat()
def fetch_agent_state_history(
auth: CXoneAuth,
tenant_url: str,
agent_id: int,
start_time: str,
end_time: str,
page_size: int = 500
) -> List[Dict]:
base_url = f"{tenant_url.rstrip('/')}/api/v2/reporting/agents/statehistory"
all_records = []
page_number = 1
has_more_pages = True
while has_more_pages:
params = {
"agentId": agent_id,
"from": start_time,
"to": end_time,
"pageSize": page_size,
"pageNumber": page_number
}
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Accept": "application/json"
}
try:
response = requests.get(
base_url,
headers=headers,
params=params,
timeout=30
)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
print(f"Rate limited. Waiting {retry_after} seconds...")
time.sleep(retry_after)
continue
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
auth.access_token = None
continue
elif response.status_code == 404:
raise Exception(f"Agent ID {agent_id} not found or no data available.") from e
else:
raise Exception(f"API Error: {response.status_code} - {response.text}") from e
except requests.exceptions.RequestException as e:
raise Exception(f"Network error: {e}") from e
data = response.json()
records = data if isinstance(data, list) else data.get("items", [])
if not records:
has_more_pages = False
break
all_records.extend(records)
if len(records) < page_size:
has_more_pages = False
else:
page_number += 1
return all_records
def analyze_state_history(records: List[Dict]) -> Dict:
summary = {
"total_records": len(records),
"state_durations": {},
"transitions": []
}
for record in records:
state_id = record.get("stateId")
duration = record.get("duration", 0)
start_time = record.get("startTime")
end_time = record.get("endTime")
description = record.get("description", "Unknown")
if state_id:
if state_id not in summary["state_durations"]:
summary["state_durations"][state_id] = 0
summary["state_durations"][state_id] += duration
summary["transitions"].append({
"state_id": state_id,
"description": description,
"duration_sec": duration,
"start": start_time,
"end": end_time
})
return summary
def print_summary(summary: Dict):
print(f"\n--- Agent State Summary ---")
print(f"Total State Changes: {summary['total_records']}")
print("\nTime Spent by State:")
sorted_states = sorted(summary['state_durations'].items(), key=lambda x: x[1], reverse=True)
for state_id, total_seconds in sorted_states:
minutes = total_seconds / 60
hours = total_seconds / 3600
print(f" State ID {state_id}: {hours:.2f} hours ({minutes:.1f} minutes)")
print("\nDetailed Transitions (Last 5):")
for t in summary['transitions'][-5:]:
print(f" [{t['start']}] -> State {t['state_id']} ({t['description']}) for {t['duration_sec']}s")
# --- Main Execution ---
if __name__ == "__main__":
if API_KEY == "YOUR_API_KEY_HERE":
print("Error: Please update the API_KEY, API_SECRET, TENANT_URL, and AGENT_ID variables in the script.")
exit(1)
try:
# 1. Initialize Auth
auth = CXoneAuth(API_KEY, API_SECRET, TENANT_URL)
# 2. Get Time Range
start_time, end_time = get_last_24_hours_range()
print(f"Fetching state history for Agent {AGENT_ID} from {start_time} to {end_time}")
# 3. Fetch Data
records = fetch_agent_state_history(auth, TENANT_URL, AGENT_ID, start_time, end_time)
# 4. Analyze and Print
summary = analyze_state_history(records)
print_summary(summary)
except Exception as e:
print(f"An error occurred: {e}")
import traceback
traceback.print_exc()
Common Errors & Debugging
Error: 401 Unauthorized
Cause: The API Key/Secret is invalid, or the OAuth token has expired and was not refreshed.
Fix:
- Verify the API Key and Secret in the CXone Admin Console under Integrations > API Keys.
- Ensure the
CXoneAuthclass is correctly encoding the credentials. - If using a long-running process, ensure the
token_expirylogic is working. The code above resets the token if401is received.
Error: 403 Forbidden
Cause: The API Key does not have the reporting scope.
Fix:
- Go to Integrations > API Keys in CXone Admin.
- Edit the API Key.
- Ensure the Reporting scope is checked.
- Save and regenerate the secret if necessary (some tenants require regeneration when scopes change).
Error: 429 Too Many Requests
Cause: You have exceeded the rate limit for the Reporting API. CXone imposes strict rate limits on reporting endpoints to protect data warehouse performance.
Fix:
- The code includes automatic retry logic with
Retry-Afterheader parsing. - Reduce the
page_sizeif you are making many small requests. - Increase the time between polling intervals if this script is scheduled.
Error: Empty Result List
Cause: The Agent ID is incorrect, or the agent was not logged in during the last 24 hours.
Fix:
- Verify the
AGENT_IDis the numeric ID, not the email. UseGET /api/v2/usersto search for the user by email to get the correct ID. - Check if the agent was actually logged in. If the agent never logged in, no state history exists.