Query NICE CXone Agent State History via Reporting API v2
What You Will Build
- A Python script that authenticates with NICE CXone and retrieves a granular history of agent state changes (e.g., Ready, Not Ready, Wrap-up) for a specific user over the last 24 hours.
- This tutorial utilizes the NICE CXone Reporting API v2, specifically the
agentStateHistoryreport type. - The implementation uses Python 3.9+ with the
requestslibrary for HTTP interactions and standard libraries for date handling.
Prerequisites
Before executing the code, ensure you have the following configured:
- NICE CXone Tenant Access: You must have a valid NICE CXone tenant URL (e.g.,
https://api.us-east-1.ic3.nice-incontact.com). - API Key Credentials: A generated API Key (Key ID and Key Secret) with sufficient permissions to read reporting data.
- OAuth Scopes: The API Key must be granted the
reports:readscope. Without this, the token generation will succeed, but the report request will return a 403 Forbidden error. - Python Environment: Python 3.9 or later installed.
- Dependencies: Install the
requestslibrary.pip install requests
Authentication Setup
NICE CXone uses OAuth 2.0 for authentication. Unlike some systems that use user/password grants, CXone API integration almost exclusively uses the Client Credentials Grant. This flow exchanges your API Key ID and Secret for a short-lived access token.
The token is valid for one hour. For a simple script running once, a fresh token is sufficient. For long-running applications, you must implement token caching and refresh logic.
Step 1: Obtain the Access Token
The authentication endpoint is /oauth/token. The request body must be application/x-www-form-urlencoded.
import requests
import base64
import json
def get_access_token(tenant_url: str, api_key_id: str, api_key_secret: str) -> str:
"""
Authenticates with NICE CXone using Client Credentials Grant.
Args:
tenant_url: The base URL of the CXone tenant (e.g., https://api.us-east-1.ic3.nice-incontact.com)
api_key_id: The API Key ID
api_key_secret: The API Key Secret
Returns:
The access token string.
Raises:
requests.exceptions.HTTPError: If authentication fails.
"""
auth_url = f"{tenant_url}/oauth/token"
# CXone expects the API Key ID and Secret in the Authorization header as Basic Auth
# during the token request, OR in the body. The standard CXone SDK approach
# puts them in the header.
credentials = f"{api_key_id}:{api_key_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"
}
response = requests.post(auth_url, headers=headers, data=data)
if response.status_code != 200:
raise requests.exceptions.HTTPError(
f"Failed to authenticate. Status: {response.status_code}, Response: {response.text}"
)
token_data = response.json()
return token_data["access_token"]
Critical Note on Endpoints: Ensure your tenant_url includes the correct region suffix (e.g., us-east-1, eu-west-1). Using the wrong region will result in a 404 or connection timeout.
Implementation
Step 2: Construct the Report Request
The NICE CXone Reporting API v2 is resource-heavy. It does not return data instantly. Instead, it uses an asynchronous pattern:
- Post a report request to create a job.
- Poll the job status until it is complete.
- Get the results.
For agent state history, we use the report type agentStateHistory.
Defining the Time Range
The API requires ISO 8601 formatted timestamps. We need the last 24 hours. We must also account for the fact that the API expects UTC.
from datetime import datetime, timedelta, timezone
def get_last_24_hours_range() -> dict:
"""
Calculates the start and end timestamps for the last 24 hours in UTC ISO 8601 format.
"""
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(hours=24)
return {
"start": start_time.isoformat(),
"end": end_time.isoformat()
}
Building the Request Body
The agentStateHistory report requires specific parameters:
- reportType:
agentStateHistory - metrics: The specific data points you want. For state history, this is usually
["stateName", "startTime", "endTime"]. - dimensions: How to group the data. Commonly
["userId", "userName"]. - filters: To limit the data to a specific agent.
def build_report_request(user_id: str, time_range: dict) -> dict:
"""
Constructs the JSON payload for the agentStateHistory report.
Args:
user_id: The unique ID of the agent (e.g., "12345678-1234-1234-1234-123456789012")
time_range: Dictionary with 'start' and 'end' ISO 8601 strings.
Returns:
The report request dictionary.
"""
return {
"reportType": "agentStateHistory",
"metrics": [
"stateName", # The name of the state (e.g., "Ready", "Not Ready")
"startTime", # When the agent entered the state
"endTime" # When the agent left the state
],
"dimensions": [
"userId", # The agent's ID
"userName" # The agent's display name
],
"filters": {
"userId": user_id # Filter strictly to this agent
},
"timeRange": {
"start": time_range["start"],
"end": time_range["end"]
},
"grouping": [
"userId",
"stateName"
]
}
Step 3: Submit the Report Job
Send the request to /api/v2/reporting/reports. This endpoint returns a jobId immediately. It does not contain the data.
def submit_report_request(tenant_url: str, access_token: str, report_request: dict) -> str:
"""
Submits the report job to CXone.
Args:
tenant_url: The base URL of the CXone tenant.
access_token: The OAuth access token.
report_request: The report configuration dictionary.
Returns:
The jobId string.
Raises:
requests.exceptions.HTTPError: If the submission fails.
"""
api_url = f"{tenant_url}/api/v2/reporting/reports"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.post(api_url, headers=headers, json=report_request)
if response.status_code not in [200, 202]:
raise requests.exceptions.HTTPError(
f"Failed to submit report. Status: {response.status_code}, Response: {response.text}"
)
# The response body contains the jobId
job_data = response.json()
return job_data["jobId"]
Step 4: Poll for Job Completion
Reports can take seconds or minutes depending on the data volume. You must poll the status endpoint: /api/v2/reporting/reports/{jobId}/status.
The status can be:
PENDING: Waiting to start.RUNNING: Processing data.COMPLETED: Data is ready.FAILED: An error occurred.
import time
MAX_POLL_ATTEMPTS = 60
POLL_INTERVAL_SECONDS = 2
def wait_for_report_completion(tenant_url: str, access_token: str, job_id: str) -> dict:
"""
Polls the report status until completion or failure.
Args:
tenant_url: The base URL of the CXone tenant.
access_token: The OAuth access token.
job_id: The ID of the submitted report job.
Returns:
The final status response dictionary.
Raises:
Exception: If the report fails or times out.
"""
status_url = f"{tenant_url}/api/v2/reporting/reports/{job_id}/status"
headers = {
"Authorization": f"Bearer {access_token}"
}
for attempt in range(MAX_POLL_ATTEMPTS):
response = requests.get(status_url, headers=headers)
if response.status_code != 200:
raise requests.exceptions.HTTPError(
f"Failed to check status. Status: {response.status_code}, Response: {response.text}"
)
status_data = response.json()
status = status_data.get("status")
if status == "COMPLETED":
return status_data
elif status == "FAILED":
raise Exception(f"Report job failed. Details: {status_data.get('message', 'Unknown error')}")
# If PENDING or RUNNING, wait and retry
print(f"Attempt {attempt + 1}/{MAX_POLL_ATTEMPTS}: Status is {status}. Waiting {POLL_INTERVAL_SECONDS}s...")
time.sleep(POLL_INTERVAL_SECONDS)
raise TimeoutError(f"Report job did not complete within {MAX_POLL_ATTEMPTS * POLL_INTERVAL_SECONDS} seconds.")
Step 5: Retrieve and Process Results
Once the status is COMPLETED, fetch the actual data from /api/v2/reporting/reports/{jobId}/results.
The result is a list of objects. Each object represents a row in the report.
def get_report_results(tenant_url: str, access_token: str, job_id: str) -> list:
"""
Retrieves the final data from a completed report job.
Args:
tenant_url: The base URL of the CXone tenant.
access_token: The OAuth access token.
job_id: The ID of the completed report job.
Returns:
A list of dictionaries containing the report rows.
"""
results_url = f"{tenant_url}/api/v2/reporting/reports/{job_id}/results"
headers = {
"Authorization": f"Bearer {access_token}"
}
response = requests.get(results_url, headers=headers)
if response.status_code != 200:
raise requests.exceptions.HTTPError(
f"Failed to retrieve results. Status: {response.status_code}, Response: {response.text}"
)
results_data = response.json()
return results_data.get("results", [])
Complete Working Example
This script combines all previous steps into a single executable module. It assumes environment variables are set for security.
import os
import requests
import base64
import time
import json
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Any
# Configuration from Environment Variables
TENANT_URL = os.getenv("CXONE_TENANT_URL", "https://api.us-east-1.ic3.nice-incontact.com")
API_KEY_ID = os.getenv("CXONE_API_KEY_ID", "YOUR_KEY_ID_HERE")
API_KEY_SECRET = os.getenv("CXONE_API_KEY_SECRET", "YOUR_KEY_SECRET_HERE")
TARGET_USER_ID = os.getenv("CXONE_TARGET_USER_ID", "12345678-1234-1234-1234-123456789012")
def get_access_token(tenant_url: str, api_key_id: str, api_key_secret: str) -> str:
auth_url = f"{tenant_url}/oauth/token"
credentials = f"{api_key_id}:{api_key_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"}
response = requests.post(auth_url, headers=headers, data=data)
response.raise_for_status()
return response.json()["access_token"]
def get_last_24_hours_range() -> Dict[str, str]:
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(hours=24)
return {
"start": start_time.isoformat(),
"end": end_time.isoformat()
}
def build_report_request(user_id: str, time_range: Dict[str, str]) -> Dict[str, Any]:
return {
"reportType": "agentStateHistory",
"metrics": ["stateName", "startTime", "endTime"],
"dimensions": ["userId", "userName"],
"filters": {
"userId": user_id
},
"timeRange": time_range,
"grouping": ["userId", "stateName"]
}
def submit_report(tenant_url: str, token: str, request_body: Dict[str, Any]) -> str:
api_url = f"{tenant_url}/api/v2/reporting/reports"
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
response = requests.post(api_url, headers=headers, json=request_body)
response.raise_for_status()
return response.json()["jobId"]
def wait_for_completion(tenant_url: str, token: str, job_id: str, max_attempts: int = 60) -> str:
status_url = f"{tenant_url}/api/v2/reporting/reports/{job_id}/status"
headers = {"Authorization": f"Bearer {token}"}
for _ in range(max_attempts):
response = requests.get(status_url, headers=headers)
response.raise_for_status()
status_data = response.json()
status = status_data.get("status")
if status == "COMPLETED":
return "COMPLETED"
if status == "FAILED":
raise Exception(f"Report failed: {status_data.get('message')}")
print(f"Status: {status}. Waiting...")
time.sleep(2)
raise TimeoutError("Report did not complete in time.")
def get_results(tenant_url: str, token: str, job_id: str) -> List[Dict[str, Any]]:
results_url = f"{tenant_url}/api/v2/reporting/reports/{job_id}/results"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(results_url, headers=headers)
response.raise_for_status()
return response.json().get("results", [])
def main():
print(f"Starting Agent State History Query for User: {TARGET_USER_ID}")
try:
# 1. Authenticate
print("1. Authenticating...")
token = get_access_token(TENANT_URL, API_KEY_ID, API_KEY_SECRET)
# 2. Prepare Request
print("2. Building report request for last 24 hours...")
time_range = get_last_24_hours_range()
report_req = build_report_request(TARGET_USER_ID, time_range)
# 3. Submit Job
print("3. Submitting report job...")
job_id = submit_report(TENANT_URL, token, report_req)
print(f" Job ID: {job_id}")
# 4. Wait for Completion
print("4. Waiting for report generation...")
final_status = wait_for_completion(TENANT_URL, token, job_id)
# 5. Retrieve Results
print("5. Fetching results...")
results = get_results(TENANT_URL, token, job_id)
# 6. Display Results
if not results:
print("No state history found for this agent in the last 24 hours.")
else:
print(f"\nFound {len(results)} state transitions.\n")
print(f"{'State Name':<15} | {'Start Time (UTC)':<25} | {'End Time (UTC)':<25}")
print("-" * 70)
for row in results:
state = row.get("stateName", "N/A")
start = row.get("startTime", "N/A")
end = row.get("endTime", "N/A")
# Format timestamps for readability if they are ISO strings
if start != "N/A":
start = start.replace("T", " ").replace("Z", "")
if end != "N/A":
end = end.replace("T", " ").replace("Z", "")
print(f"{state:<15} | {start:<25} | {end:<25}")
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden on /api/v2/reporting/reports
- Cause: The API Key used for authentication lacks the
reports:readscope. - Fix: Go to the CXone Admin Console > Security > API Keys. Edit the key and ensure the Reporting section includes Read permissions. Regenerate the key if necessary.
Error: 401 Unauthorized on Status/Results Endpoints
- Cause: The access token expired. Tokens are valid for 1 hour. If your polling loop runs for longer than an hour (unlikely for a 24-hour query, but possible for large datasets), the token may expire.
- Fix: Implement token refresh logic. Re-call
get_access_tokenif you receive a 401 during polling.
Error: Empty Results List
- Cause:
- The
user_idprovided is invalid or does not exist. - The agent was not logged in or did not change states in the specified time range.
- The time range is in the future or malformed.
- The
- Fix:
- Verify the
user_idby querying/api/v2/userswith the agent’s email or name. - Ensure the
startandendtimes are in the past. - Check that the agent actually had activity. If an agent was offline the entire time, they may not appear in state history depending on how “offline” is defined in your queue settings.
- Verify the
Error: grouping Mismatch
- Cause: The
groupingarray in the request body does not match thedimensionsormetricsstructure expected by the report type. - Fix: For
agentStateHistory, grouping byuserIdandstateNameis standard. Ensure these strings exactly match the metric/dimension names. Case sensitivity matters.