Query NICE CXone Agent State History via Reporting API v2

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 agentStateHistory report type.
  • The implementation uses Python 3.9+ with the requests library 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:read scope. 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 requests library.
    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:

  1. Post a report request to create a job.
  2. Poll the job status until it is complete.
  3. 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:read scope.
  • 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_token if you receive a 401 during polling.

Error: Empty Results List

  • Cause:
    1. The user_id provided is invalid or does not exist.
    2. The agent was not logged in or did not change states in the specified time range.
    3. The time range is in the future or malformed.
  • Fix:
    • Verify the user_id by querying /api/v2/users with the agent’s email or name.
    • Ensure the start and end times 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.

Error: grouping Mismatch

  • Cause: The grouping array in the request body does not match the dimensions or metrics structure expected by the report type.
  • Fix: For agentStateHistory, grouping by userId and stateName is standard. Ensure these strings exactly match the metric/dimension names. Case sensitivity matters.

Official References