CXone GET /agents/states returns empty array — agent not showing as logged in
What You Will Build
- You will build a Python script that authenticates with NICE CXone, queries the Agent State API, and correctly parses the response to identify active agents.
- This tutorial uses the NICE CXone REST API (
/api/v2/agents/states) and therequestslibrary. - The code is written in Python 3.9+.
Prerequisites
- OAuth Client: A CXone OAuth client with
urn:nice:cxone:agent:state:readscope. - API Version: CXone API v2.
- Runtime: Python 3.9 or higher.
- Dependencies:
requests,python-dotenv(for secure credential management).
Install dependencies:
pip install requests python-dotenv
Authentication Setup
CXone uses OAuth 2.0. You must exchange your client credentials for an access token before making API calls. The token expires after a short period (typically 3600 seconds), so your application must handle refresh tokens or re-authentication.
Create a .env file in your project root:
CXONE_CLIENT_ID=your_client_id
CXONE_CLIENT_SECRET=your_client_secret
CXONE_TENANT=your_tenant_name
The following function handles authentication and token caching. It checks if a token is still valid before requesting a new one.
import os
import time
import requests
from dotenv import load_dotenv
load_dotenv()
CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
TENANT = os.getenv("CXONE_TENANT")
# Simple in-memory cache for the token
_token_cache = {
"access_token": None,
"expires_at": 0
}
def get_access_token() -> str:
"""
Retrieves an OAuth2 access token from CXone.
Uses in-memory cache to avoid unnecessary calls within the token lifetime.
"""
current_time = time.time()
# Return cached token if it is still valid (minus 60s buffer for safety)
if _token_cache["access_token"] and current_time < _token_cache["expires_at"] - 60:
return _token_cache["access_token"]
url = f"https://{TENANT}.api.cxone.com/oauth2/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"scope": "urn:nice:cxone:agent:state:read"
}
response = requests.post(url, headers=headers, data=data, auth=(CLIENT_ID, CLIENT_SECRET))
if response.status_code != 200:
raise Exception(f"Failed to get access token: {response.status_code} - {response.text}")
token_data = response.json()
_token_cache["access_token"] = token_data["access_token"]
_token_cache["expires_at"] = current_time + token_data["expires_in"]
return _token_cache["access_token"]
Implementation
Step 1: Querying Agent States
The endpoint GET /api/v2/agents/states returns the current state of agents. A common point of confusion is that this endpoint does not return agents who are logged out. If an agent is not in the system, they will not appear in this list.
If you receive an empty array [], it means one of the following:
- No agents are currently logged in.
- The query filters are too restrictive.
- The tenant context is incorrect.
The following function fetches the raw agent states. It includes robust error handling for 401 (Unauthorized), 403 (Forbidden), and 429 (Too Many Requests).
def get_agent_states(access_token: str, site_id: str = None) -> dict:
"""
Fetches the current state of agents from CXone.
Args:
access_token: Valid OAuth2 access token.
site_id: Optional site ID to filter results. If None, fetches all sites.
Returns:
Dict containing the parsed JSON response.
"""
url = f"https://{TENANT}.api.cxone.com/api/v2/agents/states"
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json"
}
params = {}
if site_id:
params["siteId"] = site_id
response = requests.get(url, headers=headers, params=params)
# Handle specific HTTP errors
if response.status_code == 401:
raise Exception("Unauthorized: Token may be expired. Please refresh.")
elif response.status_code == 403:
raise Exception("Forbidden: Client lacks 'urn:nice:cxone:agent:state:read' scope.")
elif response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
raise Exception(f"Rate Limited. Retry after {retry_after} seconds.")
elif response.status_code != 200:
raise Exception(f"Unexpected error: {response.status_code} - {response.text}")
return response.json()
Step 2: Parsing the Response Structure
The response from /agents/states is a flat array of objects. Each object represents an agent’s current presence. A developer mistake is expecting a nested structure or assuming the agentId is the only identifier. You must look at the state object within each entry.
Key fields in the response object:
agentId: The unique identifier of the agent.state: An object containingtype(e.g.,READY,NOT_READY,PAID_BREAK) andsubState(optional, specific reason code).siteId: The site where the agent is logged in.timestamp: The last time the state was updated.
If the array is empty, verify that agents are actually logged in via the CXone Admin Console. The API only reflects agents with an active session.
Step 3: Filtering for Active Agents
To find agents who are ready to take interactions, you must filter for state.type == "READY". Agents in NOT_READY are still logged in but unavailable.
def find_ready_agents(agent_states: list) -> list:
"""
Filters the list of agent states to return only those who are READY.
"""
ready_agents = []
for agent in agent_states:
state_info = agent.get("state", {})
state_type = state_info.get("type")
# Check if the agent is in a READY state
if state_type == "READY":
ready_agents.append({
"agentId": agent.get("agentId"),
"siteId": agent.get("siteId"),
"timestamp": agent.get("timestamp")
})
return ready_agents
Complete Working Example
This script combines authentication, data fetching, and filtering. It prints the list of ready agents or reports if no agents are available.
import sys
import json
from dotenv import load_dotenv
import requests
import time
import os
# Load environment variables
load_dotenv()
CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
TENANT = os.getenv("CXONE_TENANT")
_token_cache = {
"access_token": None,
"expires_at": 0
}
def get_access_token() -> str:
current_time = time.time()
if _token_cache["access_token"] and current_time < _token_cache["expires_at"] - 60:
return _token_cache["access_token"]
url = f"https://{TENANT}.api.cxone.com/oauth2/token"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "client_credentials",
"scope": "urn:nice:cxone:agent:state:read"
}
response = requests.post(url, headers=headers, data=data, auth=(CLIENT_ID, CLIENT_SECRET))
if response.status_code != 200:
raise Exception(f"Auth failed: {response.status_code} - {response.text}")
token_data = response.json()
_token_cache["access_token"] = token_data["access_token"]
_token_cache["expires_at"] = current_time + token_data["expires_in"]
return _token_cache["access_token"]
def get_agent_states(access_token: str) -> list:
url = f"https://{TENANT}.api.cxone.com/api/v2/agents/states"
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
if response.status_code == 401:
raise Exception("Unauthorized: Token expired.")
if response.status_code == 403:
raise Exception("Forbidden: Check OAuth scopes.")
if response.status_code == 429:
raise Exception("Rate Limited.")
if response.status_code != 200:
raise Exception(f"Error: {response.status_code} - {response.text}")
return response.json()
def main():
try:
# 1. Authenticate
token = get_access_token()
print("Authentication successful.")
# 2. Fetch States
print("Fetching agent states...")
agent_states = get_agent_states(token)
# 3. Analyze Results
if not agent_states:
print("Result: Empty array returned.")
print("Debugging Checklist:")
print("1. Are any agents currently logged in to the CXone desktop?")
print("2. Is the tenant name in the URL correct?")
print("3. Check the Admin Console > Users to confirm login status.")
return
print(f"Total agents returned: {len(agent_states)}")
# 4. Filter for Ready Agents
ready_agents = []
not_ready_agents = []
for agent in agent_states:
state_type = agent.get("state", {}).get("type")
agent_id = agent.get("agentId")
if state_type == "READY":
ready_agents.append(agent_id)
else:
not_ready_agents.append({"id": agent_id, "state": state_type})
# 5. Output Results
print(f"\nReady Agents ({len(ready_agents)}):")
for agent_id in ready_agents:
print(f" - {agent_id}")
print(f"\nNot Ready/Other States ({len(not_ready_agents)}):")
for item in not_ready_agents:
print(f" - Agent {item['id']} is: {item['state']}")
except Exception as e:
print(f"Execution failed: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: Empty Array []
Cause: The API only returns agents who have an active session in the CXone platform. If no agents are logged in, the array is empty. This is expected behavior, not an error.
Fix:
- Log in to the CXone Admin Console.
- Navigate to Users and verify that at least one user has a status of “Logged In”.
- Ensure the agent has logged into the CXone Desktop application or Web Client.
- If agents are logged in but still missing, check the
siteId. If you filter bysiteId, ensure the agent is logged into that specific site.
Error: 403 Forbidden
Cause: The OAuth client does not have the required scope urn:nice:cxone:agent:state:read.
Fix:
- Go to the CXone Admin Console.
- Navigate to Security > OAuth Clients.
- Select your client.
- Add the scope
urn:nice:cxone:agent:state:readto the allowed scopes. - Regenerate the token.
Error: 429 Too Many Requests
Cause: You have exceeded the rate limit for the /agents/states endpoint. CXone enforces strict rate limits to protect platform stability.
Fix:
Implement exponential backoff. The response header Retry-After indicates the number of seconds to wait.
import time
def fetch_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
wait_time = int(response.headers.get("Retry-After", 2 ** attempt))
print(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time)
continue
return response
return response