CXone Reporting API (v2) — how to query agent state history for the last 24 hours
What You Will Build
- One sentence: The code retrieves a time-series dataset of agent presence states (e.g., Available, Wrap-Up, Offline) for a specific agent over the previous 24 hours.
- One sentence: This uses the NICE CXone Reporting API v2, specifically the
/api/v2/reporting/agent-presenceendpoint. - One sentence: The programming language covered is Python 3.10+ using the
requestslibrary.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Grant).
- Required Scopes:
reporting:readorreporting:agent:read. Without this scope, the API returns403 Forbidden. - SDK/API Version: CXone REST API v2. No specific SDK is required; we will use raw HTTP requests for maximum transparency, which allows you to adapt the logic to any SDK later.
- Language/Runtime: Python 3.10 or higher.
- External Dependencies:
requests(for HTTP calls)python-dotenv(for secure credential management)pytz(for robust timezone handling)
Install dependencies via pip:
pip install requests python-dotenv pytz
Authentication Setup
NICE CXone uses OAuth 2.0 Client Credentials flow. You must obtain an access token before querying reporting data. The token is valid for one hour. For a production application, you must cache the token and refresh it before expiration to avoid 401 Unauthorized errors during data retrieval.
Create a .env file in your project root with your credentials:
# .env
CXONE_TENANT_DOMAIN=yourtenant.com
CXONE_CLIENT_ID=your_client_id_here
CXONE_CLIENT_SECRET=your_client_secret_here
CXONE_AGENT_ID=1234567890123456789
Below is the authentication logic. It handles the initial token request and includes a simple caching mechanism.
import os
import time
import requests
from dotenv import load_dotenv
load_dotenv()
CXONE_DOMAIN = os.getenv("CXONE_TENANT_DOMAIN")
CXONE_CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CXONE_CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
BASE_URL = f"https://{CXONE_DOMAIN}"
TOKEN_URL = f"{BASE_URL}/oauth/token"
# Simple in-memory cache for the token
_token_cache = {
"access_token": None,
"expires_at": 0
}
def get_access_token() -> str:
"""
Retrieves an OAuth access token.
Returns the cached token if it is still valid, otherwise requests a new one.
"""
current_time = time.time()
# Check if we have a valid cached token
if _token_cache["access_token"] and current_time < _token_cache["expires_at"]:
return _token_cache["access_token"]
# Request a new token
payload = {
"grant_type": "client_credentials",
"client_id": CXONE_CLIENT_ID,
"client_secret": CXONE_CLIENT_SECRET
}
try:
response = requests.post(TOKEN_URL, data=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("Authentication failed: Invalid Client ID or Secret.") from e
elif response.status_code == 403:
raise Exception("Authentication failed: Client lacks permission or is disabled.") from e
raise Exception(f"Authentication failed with status {response.status_code}: {response.text}") from e
data = response.json()
access_token = data["access_token"]
expires_in = data["expires_in"]
# Cache the token with a small buffer (10 seconds) to prevent edge-case expiration
_token_cache["access_token"] = access_token
_token_cache["expires_at"] = current_time + (expires_in - 10)
return access_token
Implementation
Step 1: Constructing the Time Range and Request Parameters
The CXone Reporting API requires ISO 8601 timestamps for time-based queries. A common pitfall is using local time without timezone indicators, which the API may interpret as UTC, resulting in data shifts. We will use pytz to ensure the timestamps are explicitly UTC.
We need to calculate the start time (24 hours ago) and the end time (now).
import pytz
from datetime import datetime, timedelta
def get_last_24h_timestamps():
"""
Returns start and end timestamps in ISO 8601 format (UTC).
"""
utc_now = datetime.now(pytz.utc)
utc_start = utc_now - timedelta(hours=24)
# Format as ISO 8601 with 'Z' suffix for UTC
start_iso = utc_start.isoformat().replace("+00:00", "Z")
end_iso = utc_now.isoformat().replace("+00:00", "Z")
return start_iso, end_iso
Step 2: Querying the Agent Presence Endpoint
The endpoint for agent state history is GET /api/v2/reporting/agent-presence.
Key Parameters:
agentIds: A comma-separated list of agent IDs. We will pass a single ID.startTime&endTime: The ISO 8601 strings calculated above.interval: The granularity of the data. Options includePT1H(1 hour),PT1M(1 minute), etc. For a 24-hour view,PT1His efficient. For detailed state transitions, you might usePT1M, but be aware of pagination limits.states: Optional. You can filter for specific states likeAvailable,WrapUp,Busy,Offline. If omitted, all states are returned.
Required Scope: reporting:read
import requests
CXONE_AGENT_ID = os.getenv("CXONE_AGENT_ID")
def fetch_agent_presence_history(agent_id: str, start_time: str, end_time: str, interval: str = "PT1H") -> dict:
"""
Queries the CXone Reporting API for agent presence history.
Handles pagination if the result set is large.
"""
url = f"{BASE_URL}/api/v2/reporting/agent-presence"
# Get a fresh token for this request
token = get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json"
}
params = {
"agentIds": agent_id,
"startTime": start_time,
"endTime": end_time,
"interval": interval,
# Optional: Filter specific states. Remove this line to get all states.
# "states": "Available,WrapUp,Offline"
}
all_data = []
next_page_token = None
while True:
if next_page_token:
params["nextPageToken"] = next_page_token
try:
response = requests.get(url, headers=headers, params=params)
# Handle Token Expiration mid-request
if response.status_code == 401:
print("Token expired during request. Refreshing...")
get_access_token() # Force refresh
headers["Authorization"] = f"Bearer {get_access_token()}"
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if response.status_code == 429:
# Rate Limiting: CXone uses 429 with a Retry-After header
retry_after = int(response.headers.get("Retry-After", 1))
print(f"Rate limited (429). Waiting {retry_after} seconds...")
time.sleep(retry_after)
continue
elif response.status_code == 403:
raise Exception("Forbidden: Check if your client has 'reporting:read' scope.") from e
elif response.status_code == 400:
raise Exception(f"Bad Request: {response.text}") from e
else:
raise Exception(f"HTTP Error {response.status_code}: {response.text}") from e
data = response.json()
# Accumulate results
if "data" in data and data["data"]:
all_data.extend(data["data"])
# Check for pagination
next_page_token = data.get("nextPageToken")
if not next_page_token:
break
return all_data
Step 3: Processing and Formatting the Results
The API returns a list of objects. Each object represents a time bucket. The structure typically looks like this:
{
"agentId": "1234567890",
"startTime": "2023-10-27T10:00:00Z",
"endTime": "2023-10-27T11:00:00Z",
"states": [
{
"state": "Available",
"duration": 3600,
"percentage": 100.0
}
]
}
We need to flatten this data to make it usable. We will create a summary table showing the total duration (in seconds) spent in each state for the agent over the last 24 hours.
from collections import defaultdict
def analyze_presence_data(raw_data: list) -> dict:
"""
Aggregates raw API response into a summary of total duration per state.
"""
state_durations = defaultdict(float)
for entry in raw_data:
# Ensure the entry has state data
if "states" not in entry:
continue
for state_info in entry["states"]:
state_name = state_info.get("state", "Unknown")
duration = state_info.get("duration", 0)
# Accumulate duration
state_durations[state_name] += duration
return dict(state_durations)
def print_summary(agent_id: str, state_durations: dict):
"""
Prints a formatted summary of the agent's presence.
"""
print(f"\n--- Agent Presence Summary (Last 24h) ---")
print(f"Agent ID: {agent_id}")
print(f"{'State':<15} | {'Duration (sec)':<15} | {'Hours':<10}")
print("-" * 45)
for state, duration in sorted(state_durations.items(), key=lambda x: x[1], reverse=True):
hours = duration / 3600
print(f"{state:<15} | {duration:<15.2f} | {hours:<10.2f}")
total_duration = sum(state_durations.values())
print("-" * 45)
print(f"{'Total':<15} | {total_duration:<15.2f} | {total_duration/3600:<10.2f}")
Complete Working Example
Below is the full, copy-pasteable script. Save this as cxone_agent_history.py.
import os
import time
import requests
import pytz
from datetime import datetime, timedelta
from collections import defaultdict
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Configuration
CXONE_DOMAIN = os.getenv("CXONE_TENANT_DOMAIN")
CXONE_CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CXONE_CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
CXONE_AGENT_ID = os.getenv("CXONE_AGENT_ID")
if not all([CXONE_DOMAIN, CXONE_CLIENT_ID, CXONE_CLIENT_SECRET, CXONE_AGENT_ID]):
raise ValueError("Missing required environment variables. Check your .env file.")
BASE_URL = f"https://{CXONE_DOMAIN}"
TOKEN_URL = f"{BASE_URL}/oauth/token"
# Token Cache
_token_cache = {
"access_token": None,
"expires_at": 0
}
def get_access_token() -> str:
"""
Retrieves an OAuth access token using Client Credentials flow.
Implements basic caching to avoid unnecessary token requests.
"""
current_time = time.time()
if _token_cache["access_token"] and current_time < _token_cache["expires_at"]:
return _token_cache["access_token"]
payload = {
"grant_type": "client_credentials",
"client_id": CXONE_CLIENT_ID,
"client_secret": CXONE_CLIENT_SECRET
}
try:
response = requests.post(TOKEN_URL, data=payload, timeout=10)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("Authentication failed: Invalid Client ID or Secret.") from e
elif response.status_code == 403:
raise Exception("Authentication failed: Client lacks permission or is disabled.") from e
raise Exception(f"Authentication failed with status {response.status_code}: {response.text}") from e
except requests.exceptions.RequestException as e:
raise Exception(f"Network error during token request: {e}") from e
data = response.json()
access_token = data["access_token"]
expires_in = data["expires_in"]
_token_cache["access_token"] = access_token
_token_cache["expires_at"] = current_time + (expires_in - 10)
return access_token
def get_last_24h_timestamps():
"""
Returns start and end timestamps in ISO 8601 format (UTC).
"""
utc_now = datetime.now(pytz.utc)
utc_start = utc_now - timedelta(hours=24)
start_iso = utc_start.isoformat().replace("+00:00", "Z")
end_iso = utc_now.isoformat().replace("+00:00", "Z")
return start_iso, end_iso
def fetch_agent_presence_history(agent_id: str, start_time: str, end_time: str, interval: str = "PT1H") -> list:
"""
Queries the CXone Reporting API for agent presence history.
Handles pagination and rate limiting (429).
"""
url = f"{BASE_URL}/api/v2/reporting/agent-presence"
token = get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json"
}
params = {
"agentIds": agent_id,
"startTime": start_time,
"endTime": end_time,
"interval": interval
}
all_data = []
next_page_token = None
while True:
if next_page_token:
params["nextPageToken"] = next_page_token
try:
response = requests.get(url, headers=headers, params=params, timeout=30)
# Handle Token Expiration mid-request
if response.status_code == 401:
print("Token expired during request. Refreshing...")
get_access_token()
headers["Authorization"] = f"Bearer {get_access_token()}"
response = requests.get(url, headers=headers, params=params, timeout=30)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 1))
print(f"Rate limited (429). Waiting {retry_after} seconds...")
time.sleep(retry_after)
continue
elif response.status_code == 403:
raise Exception("Forbidden: Check if your client has 'reporting:read' scope.") from e
elif response.status_code == 400:
raise Exception(f"Bad Request: {response.text}") from e
else:
raise Exception(f"HTTP Error {response.status_code}: {response.text}") from e
except requests.exceptions.Timeout:
raise Exception("Request timed out. Check network connectivity.")
data = response.json()
if "data" in data and data["data"]:
all_data.extend(data["data"])
next_page_token = data.get("nextPageToken")
if not next_page_token:
break
return all_data
def analyze_presence_data(raw_data: list) -> dict:
"""
Aggregates raw API response into a summary of total duration per state.
"""
state_durations = defaultdict(float)
for entry in raw_data:
if "states" not in entry:
continue
for state_info in entry["states"]:
state_name = state_info.get("state", "Unknown")
duration = state_info.get("duration", 0)
state_durations[state_name] += duration
return dict(state_durations)
def print_summary(agent_id: str, state_durations: dict):
"""
Prints a formatted summary of the agent's presence.
"""
print(f"\n--- Agent Presence Summary (Last 24h) ---")
print(f"Agent ID: {agent_id}")
print(f"{'State':<15} | {'Duration (sec)':<15} | {'Hours':<10}")
print("-" * 45)
for state, duration in sorted(state_durations.items(), key=lambda x: x[1], reverse=True):
hours = duration / 3600
print(f"{state:<15} | {duration:<15.2f} | {hours:<10.2f}")
total_duration = sum(state_durations.values())
print("-" * 45)
print(f"{'Total':<15} | {total_duration:<15.2f} | {total_duration/3600:<10.2f}")
def main():
try:
print("Generating timestamps...")
start_time, end_time = get_last_24h_timestamps()
print(f"Querying period: {start_time} to {end_time}")
print("Fetching agent presence history...")
raw_data = fetch_agent_presence_history(
agent_id=CXONE_AGENT_ID,
start_time=start_time,
end_time=end_time,
interval="PT1H" # 1-hour intervals
)
if not raw_data:
print("No presence data found for this agent in the last 24 hours.")
return
print("Analyzing data...")
summary = analyze_presence_data(raw_data)
print_summary(CXONE_AGENT_ID, summary)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
What causes it: The OAuth client used to generate the token does not have the reporting:read scope assigned.
How to fix it:
- Log in to the CXone Admin Portal.
- Navigate to Security > API Clients.
- Select your client.
- Edit the scopes. Ensure
reporting:readis checked. - Save and regenerate the token.
Error: 429 Too Many Requests
What causes it: You have exceeded the rate limit for the Reporting API. CXone enforces strict limits on reporting endpoints to protect backend analytics databases.
How to fix it:
The code above handles this automatically by reading the Retry-After header and sleeping. If you are building a high-volume dashboard, implement exponential backoff. Do not hammer the endpoint.
Error: Empty Data Response
What causes it:
- The agent ID is incorrect.
- The agent has not logged in or changed states in the last 24 hours.
- The time range is in the future.
How to fix it:
Verify theCXONE_AGENT_IDin the Admin Portal under Workforce Management > Agents. Ensure the ID matches exactly. Check the console output to verify thestartTimeandendTimeare correct.
Error: 400 Bad Request (Invalid Interval)
What causes it: The interval parameter does not follow ISO 8601 duration format.
How to fix it:
Use valid formats such as PT1H (1 hour), PT1M (1 minute), PT5M (5 minutes), or P1D (1 day). Ensure the interval is not too small for the requested time range (e.g., PT1M over 24 hours may return too much data and trigger pagination limits).