Troubleshooting CXone GET /agents/states Returning Empty Arrays

Troubleshooting CXone GET /agents/states Returning Empty Arrays

What You Will Build

  • A diagnostic Python script that queries the NICE CXone Agent States API to retrieve real-time availability for a specific agent.
  • Logic to validate OAuth token scopes and filter results to identify why an agent appears offline or missing from the response.
  • Python code using the requests library with explicit error handling for 401, 403, and 429 responses.

Prerequisites

  • OAuth Client Type: Server-to-Server (Client Credentials) or User-to-Server (OAuth2) with active session.
  • Required Scopes: agents:agent:read and agents:state:read. Without these, the API returns an empty array or a 403 Forbidden error.
  • SDK/API Version: NICE CXone REST API v2.
  • Language/Runtime: Python 3.8+.
  • External Dependencies: requests, pyjwt (optional, for token debugging).
pip install requests pyjwt

Authentication Setup

The most common cause of an empty array in CXone is an OAuth token that lacks the specific agents:state:read scope. CXone uses fine-grained scopes. A token with agents:agent:read allows you to see agent profiles but not their real-time state.

This section demonstrates how to obtain a token and verify its scopes before making the API call.

import requests
import json
import jwt

# Configuration
CXONE_DOMAIN = "niceincontact.com" # Change to your region, e.g., nicecxone.com
CLIENT_ID = "YOUR_CLIENT_ID"
CLIENT_SECRET = "YOUR_CLIENT_SECRET"
AUTH_URL = f"https://{CLIENT_ID}.{CXONE_DOMAIN}/oauth/token"

def get_access_token():
    """
    Retrieves an OAuth2 access token using Client Credentials flow.
    """
    payload = {
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "scope": "agents:agent:read agents:state:read" # Critical: Include state:read
    }

    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }

    try:
        response = requests.post(AUTH_URL, data=payload, headers=headers)
        response.raise_for_status()
        token_data = response.json()
        return token_data["access_token"]
    except requests.exceptions.HTTPError as e:
        print(f"Authentication failed: {e.response.status_code} - {e.response.text}")
        raise

def verify_token_scopes(token: str):
    """
    Decodes the JWT to verify it contains the required scopes.
    """
    try:
        # CXone tokens are signed with RS256. We decode without verification for inspection.
        # In production, verify the signature using the public key from the JWKS endpoint.
        decoded = jwt.decode(token, options={"verify_signature": False})
        scopes = decoded.get("scope", "")
        print(f"Token Scopes: {scopes}")
        
        required_scopes = ["agents:agent:read", "agents:state:read"]
        for req_scope in required_scopes:
            if req_scope not in scopes:
                raise ValueError(f"Missing required scope: {req_scope}")
        return True
    except Exception as e:
        print(f"Token verification failed: {e}")
        return False

# Initial Auth
token = get_access_token()
if verify_token_scopes(token):
    print("Token is valid and has required scopes.")
else:
    exit(1)

Implementation

Step 1: Constructing the Agent States Request

The endpoint GET /api/v2/agents/states returns a list of all agents currently in the system who have an active state. It does not return agents who are completely logged out (state is null/empty).

Key Parameter: agentIds
If you do not provide agentIds, the API returns all agents with a state. If you are looking for a specific agent and do not see them, they are either logged out or the ID is incorrect.

Endpoint: GET /api/v2/agents/states
Method: GET
Headers: Authorization: Bearer <token>, Content-Type: application/json

def get_agent_states(token: str, agent_ids: list[str] = None):
    """
    Fetches agent states from CXone.
    
    Args:
        token: Valid OAuth2 access token.
        agent_ids: Optional list of agent IDs to filter by. If None, returns all active agents.
    
    Returns:
        dict: The JSON response from the API.
    """
    base_url = f"https://{CLIENT_ID}.{CXONE_DOMAIN}/api/v2/agents/states"
    
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json"
    }

    params = {}
    if agent_ids:
        # CXone API expects comma-separated IDs for this endpoint
        params["agentIds"] = ",".join(agent_ids)

    try:
        response = requests.get(base_url, headers=headers, params=params)
        
        # Handle 429 Rate Limiting
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After", 1)
            print(f"Rate limited (429). Retrying after {retry_after} seconds...")
            import time
            time.sleep(int(retry_after))
            response = requests.get(base_url, headers=headers, params=params)
        
        response.raise_for_status()
        return response.json()
    
    except requests.exceptions.HTTPError as e:
        print(f"API Error: {e.response.status_code}")
        print(f"Response Body: {e.response.text}")
        
        if e.response.status_code == 401:
            print("Error: Token is invalid or expired. Re-authenticate.")
        elif e.response.status_code == 403:
            print("Error: Forbidden. Check if 'agents:state:read' scope is present.")
        elif e.response.status_code == 404:
            print("Error: Endpoint not found. Check base URL and subdomain.")
        raise
    except Exception as e:
        print(f"Unexpected error: {e}")
        raise

# Example Usage
agent_id_to_find = "12345678-1234-1234-1234-123456789012" # Replace with actual UUID
result = get_agent_states(token, agent_ids=[agent_id_to_find])
print(json.dumps(result, indent=2))

Step 2: Analyzing the Response Structure

The response is a JSON object containing an items array. Each item represents an agent with a current state.

Realistic Response Body (Agent Logged In):

{
    "items": [
        {
            "id": "12345678-1234-1234-1234-123456789012",
            "name": "John Doe",
            "email": "john.doe@example.com",
            "state": {
                "id": "8b7c6d5e-4f3a-2b1c-0d9e-8f7a6b5c4d3e",
                "name": "Available",
                "description": "Ready to receive interactions",
                "color": "#00FF00",
                "skillLevels": [
                    {
                        "skillId": "skill-uuid-123",
                        "name": "English",
                        "level": 100
                    }
                ]
            },
            "lastUpdatedTimestamp": "2023-10-27T10:00:00.000Z"
        }
    ],
    "page": 1,
    "pageSize": 100,
    "totalElements": 1,
    "totalPages": 1
}

Realistic Response Body (Agent NOT in response):

{
    "items": [],
    "page": 1,
    "pageSize": 100,
    "totalElements": 0,
    "totalPages": 0
}

If you receive items: [] when querying a specific agentId, the agent is not in the system with an active state. This means they are logged out, or the ID is wrong.

Step 3: Debugging “Empty Array” Scenarios

There are three primary reasons for an empty array when you expect an agent to be present. We will build a diagnostic function to rule them out.

  1. Wrong Agent ID: You are querying by Email or Name, but the API requires the UUID.
  2. Agent is Logged Out: The agent has no active state record. The API only returns agents with a state.
  3. Skill/Queue Mismatch (Advanced): In some configurations, if an agent is in a state that has no skill levels defined, they may appear differently, but they should still appear in the list.
def diagnose_agent_visibility(token: str, search_identifier: str):
    """
    Diagnoses why an agent is not appearing in the states list.
    
    Args:
        token: Valid OAuth2 access token.
        search_identifier: The Agent ID, Email, or Name to search for.
    """
    base_url = f"https://{CLIENT_ID}.{CXONE_DOMAIN}"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    print(f"\n--- Diagnosing Agent: {search_identifier} ---")

    # Step 1: Verify Agent Exists and Get UUID
    # We use the Agent Management API to find the UUID by email or name if ID is not provided
    agent_uuid = None
    
    # Check if input looks like a UUID
    if len(search_identifier) == 36 and search_identifier.count('-') == 4:
        agent_uuid = search_identifier
        print("Input identified as UUID. Proceeding to state check.")
    else:
        print("Input not a UUID. Searching Agent Management API...")
        # Search agents by email or name
        search_url = f"{base_url}/api/v2/agents"
        params = {
            "filter": f"email eq '{search_identifier}' or name eq '{search_identifier}'",
            "pageSize": 10
        }
        
        resp = requests.get(search_url, headers=headers, params=params)
        if resp.status_code == 200:
            data = resp.json()
            if data.get("items"):
                agent_uuid = data["items"][0]["id"]
                print(f"Found Agent UUID: {agent_uuid}")
            else:
                print("ERROR: Agent not found in CXone directory. Check spelling or existence.")
                return
        else:
            print(f"ERROR: Failed to search agents. Status: {resp.status_code}")
            return

    # Step 2: Check State with Correct UUID
    if agent_uuid:
        state_url = f"{base_url}/api/v2/agents/states"
        params = {"agentIds": agent_uuid}
        
        resp = requests.get(state_url, headers=headers, params=params)
        
        if resp.status_code == 200:
            state_data = resp.json()
            if state_data.get("items"):
                agent_state = state_data["items"][0]
                print(f"SUCCESS: Agent is ONLINE.")
                print(f"Current State: {agent_state['state']['name']}")
                print(f"Last Updated: {agent_state['lastUpdatedTimestamp']}")
            else:
                print("RESULT: Agent exists but has NO ACTIVE STATE.")
                print("Explanation: The agent is logged out or in a 'offline' state that CXone does not track in the /states endpoint.")
                print("Action: Ask the agent to log in and select a state (e.g., Available, Break).")
        else:
            print(f"ERROR: Failed to fetch state. Status: {resp.status_code}")
            print(f"Response: {resp.text}")

diagnose_agent_visibility(token, "agent.email@company.com")

Complete Working Example

This script combines authentication, scope verification, and diagnostic logic into a single runnable module.

import requests
import json
import sys
import time

# --- CONFIGURATION ---
CXONE_SUBDOMAIN = "your-subdomain" # e.g., niceincontact.com
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
SEARCH_TERM = "agent.email@company.com" # Or UUID

# --- HELPER FUNCTIONS ---

def get_token():
    url = f"https://{CLIENT_ID}.{CXONE_SUBDOMAIN}/oauth/token"
    data = {
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "scope": "agents:agent:read agents:state:read"
    }
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    
    try:
        r = requests.post(url, data=data, headers=headers)
        r.raise_for_status()
        return r.json()["access_token"]
    except Exception as e:
        print(f"Auth Failed: {e}")
        sys.exit(1)

def get_agent_uuid(token, search_term):
    """Finds UUID by email/name if not a UUID."""
    is_uuid = len(search_term) == 36 and search_term.count('-') == 4
    if is_uuid:
        return search_term
    
    url = f"https://{CLIENT_ID}.{CXONE_SUBDOMAIN}/api/v2/agents"
    headers = {"Authorization": f"Bearer {token}"}
    params = {
        "filter": f"email eq '{search_term}' or name eq '{search_term}'",
        "pageSize": 5
    }
    
    r = requests.get(url, headers=headers, params=params)
    if r.status_code == 200:
        items = r.json().get("items", [])
        if items:
            return items[0]["id"]
        else:
            print("Agent not found in directory.")
            return None
    else:
        print(f"Agent search failed: {r.status_code}")
        return None

def check_agent_state(token, agent_id):
    """Checks if agent has an active state."""
    url = f"https://{CLIENT_ID}.{CXONE_SUBDOMAIN}/api/v2/agents/states"
    headers = {"Authorization": f"Bearer {token}"}
    params = {"agentIds": agent_id}
    
    r = requests.get(url, headers=headers, params=params)
    
    if r.status_code == 429:
        print("Rate limited. Waiting 1s...")
        time.sleep(1)
        r = requests.get(url, headers=headers, params=params)
        
    if r.status_code != 200:
        print(f"State API Error: {r.status_code} - {r.text}")
        return False
        
    data = r.json()
    if data.get("items"):
        return data["items"][0]
    return None

# --- MAIN EXECUTION ---

if __name__ == "__main__":
    print("Starting CXone Agent State Diagnostic...")
    
    # 1. Authenticate
    token = get_token()
    print("Authenticated successfully.")
    
    # 2. Find Agent UUID
    agent_id = get_agent_uuid(token, SEARCH_TERM)
    if not agent_id:
        print("Cannot proceed. Agent not found.")
        sys.exit(1)
        
    print(f"Found Agent ID: {agent_id}")
    
    # 3. Check State
    state_info = check_agent_state(token, agent_id)
    
    if state_info:
        print("\n--- RESULT: Agent is ACTIVE ---")
        print(f"State Name: {state_info['state']['name']}")
        print(f"State ID: {state_info['state']['id']}")
        print(f"Last Updated: {state_info['lastUpdatedTimestamp']}")
    else:
        print("\n--- RESULT: Agent is INACTIVE ---")
        print("The agent is not returned in the /agents/states list.")
        print("This means the agent is currently LOGGED OUT.")
        print("To fix this, the agent must log into the CXone Desktop/Agent Workspace.")

Common Errors & Debugging

Error: 403 Forbidden

What causes it: The OAuth token does not have the agents:state:read scope.
How to fix it: Update your OAuth client’s allowed scopes in the CXone Admin Console under Integrations > OAuth Clients. Add agents:state:read. Re-generate the token.

Error: 401 Unauthorized

What causes it: The token is expired, invalid, or the subdomain in the URL is incorrect.
How to fix it: Ensure CXONE_SUBDOMAIN matches your organization’s URL exactly. Check that the CLIENT_SECRET is correct. Tokens expire after 3600 seconds (1 hour) by default; implement token refresh logic.

Error: Empty items Array (200 OK)

What causes it:

  1. Agent Logged Out: The API only returns agents with an active state. Logged-out agents do not appear.
  2. Wrong ID: You are passing an Email or Name instead of the UUID.
  3. Filter Mismatch: If you use query parameters incorrectly, the filter may exclude the agent.

Debugging Code:

# Add this debug block to your check_agent_state function
if not state_info:
    print("DEBUG: Querying ALL agents to see if anyone is online...")
    all_states_url = f"https://{CLIENT_ID}.{CXONE_SUBDOMAIN}/api/v2/agents/states"
    r = requests.get(all_states_url, headers={"Authorization": f"Bearer {token}"})
    if r.status_code == 200:
        all_data = r.json()
        print(f"Total online agents: {all_data.get('totalElements', 0)}")
        if all_data.get('items'):
            print("Sample online agent:", all_data['items'][0]['name'])
        else:
            print("No agents are online in the system.")

Error: 429 Too Many Requests

What causes it: CXone enforces strict rate limits on the Agent States API (typically 10 requests per second per client).
How to fix it: Implement exponential backoff. The code above includes a basic retry for 429s. For production, use a library like tenacity or backoff.

Official References