Debugging Empty Agent States in NICE CXone: Why /agents/states Returns []
What You Will Build
- You will build a diagnostic script that queries the NICE CXone
/agents/statesendpoint to retrieve real-time agent availability. - You will identify the three most common causes for empty arrays: incorrect
env_uuid, insufficient OAuth scopes, and agent login status mismatches. - You will use Python with the
requestslibrary to implement robust error handling and pagination for state retrieval.
Prerequisites
- OAuth Client: A NICE CXone OAuth client with the
read:agentscope. Theread:agentscope is strictly required for/agents/states. The genericread:profilescope is insufficient. - Environment UUID: The specific
env_uuidfor the NICE CXone environment. This is distinct from thesubdomain. - Runtime: Python 3.8 or higher.
- Dependencies:
requests(installed viapip install requests). - Agent Status: At least one agent must be explicitly logged into a skill or general mode within the CXone UI for the endpoint to return data.
Authentication Setup
NICE CXone uses standard OAuth 2.0 Client Credentials flow. The token endpoint is https://login.nicecxone.com/oauth2/token.
The following function handles the token acquisition. It caches the token for 55 minutes (the token expires in 60) to avoid unnecessary refresh calls and handles the 401 error by forcing a refresh.
import requests
import time
import json
class CXoneAuth:
def __init__(self, client_id: str, client_secret: str, subdomain: str):
self.client_id = client_id
self.client_secret = client_secret
self.subdomain = subdomain
self.token_url = "https://login.nicecxone.com/oauth2/token"
self.api_base_url = f"https://{subdomain}.api.nicecxone.com"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
"""
Retrieves or refreshes the OAuth2 access token.
"""
# If we have a valid token, return it
if self.access_token and time.time() < self.token_expiry:
return self.access_token
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "read:agent" # Critical: Must include read:agent
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
try:
response = requests.post(self.token_url, data=payload, headers=headers)
response.raise_for_status()
data = response.json()
self.access_token = data["access_token"]
# Set expiry to 55 minutes to provide a 5-minute safety buffer
self.token_expiry = time.time() + (55 * 60)
return self.access_token
except requests.exceptions.HTTPError as e:
if response.status_code == 400:
raise ValueError("Invalid client credentials or grant type.")
elif response.status_code == 401:
raise ValueError("Unauthorized. Check Client ID and Secret.")
else:
raise Exception(f"OAuth Error: {e.response.text}")
def get_headers(self) -> dict:
"""
Returns headers with the current valid access token.
"""
token = self.get_token()
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
Implementation
Step 1: Verify Environment UUID and Basic Connectivity
The most frequent cause of an empty array is querying the wrong environment. NICE CXone environments are isolated by env_uuid. If you use the subdomain in the URL but the wrong UUID in the query parameters, or vice versa, the API may return an empty list if no agents match the implicit filter.
First, we establish the connection and verify the env_uuid. You can find the env_uuid in the NICE CXone Admin Console under Settings > Environment > Environment ID.
def verify_connection(auth: CXoneAuth, env_uuid: str):
"""
Performs a basic GET request to verify the environment is reachable.
"""
url = f"{auth.api_base_url}/api/v2/agents/states"
# The env_uuid is often passed as a query parameter or derived from the subdomain.
# However, for /agents/states, the subdomain in the URL is the primary identifier.
# We pass env_uuid explicitly if the API version requires it, but v2 usually
# relies on the subdomain context. Let's check the specific endpoint behavior.
# Note: In CXone v2, /agents/states does NOT require env_uuid in the query
# if the subdomain is correct. However, some older endpoints do.
# We will test the direct call.
headers = auth.get_headers()
try:
response = requests.get(url, headers=headers)
if response.status_code == 401:
print("ERROR: 401 Unauthorized. Check OAuth scopes.")
return False
elif response.status_code == 403:
print("ERROR: 403 Forbidden. Client lacks 'read:agent' scope.")
return False
elif response.status_code == 404:
print("ERROR: 404 Not Found. Subdomain may be incorrect.")
return False
elif response.status_code == 200:
data = response.json()
# Check if the response structure is valid
if "entities" in data:
print(f"Connection successful. Found {len(data['entities'])} agents.")
return True
else:
print("Unexpected response structure.")
print(json.dumps(data, indent=2))
return False
else:
print(f"Unexpected Status Code: {response.status_code}")
print(response.text)
return False
except requests.exceptions.RequestException as e:
print(f"Network Error: {e}")
return False
Step 2: Query Agent States with Explicit Filters
If the connection is successful but the array is empty, the issue is likely one of the following:
- No Agents are Logged In: The
/agents/statesendpoint only returns agents who are currently logged into the system. If an agent is “Offline” or “Not Logged In”, they will not appear in this list. - Skill-Based Filtering: By default, the endpoint might return all logged-in agents. However, if you are using query parameters to filter by skill, ensure the skill ID is correct.
- Pagination Limits: The default page size is 25. If you have more than 25 agents, you must paginate.
We will implement a query that retrieves all logged-in agents, regardless of skill, to isolate the “empty array” issue.
def get_all_agent_states(auth: CXoneAuth, page_size: int = 100) -> list:
"""
Retrieves all currently logged-in agent states with pagination.
"""
all_agents = []
page = 1
url = f"{auth.api_base_url}/api/v2/agents/states"
headers = auth.get_headers()
while True:
params = {
"page_size": page_size,
"page": page
}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
entities = data.get("entities", [])
if not entities:
# No more agents or empty list
break
all_agents.extend(entities)
# Check if there are more pages
# CXone v2 pagination uses 'next_page' link in the response or
# checks if len(entities) < page_size
if len(entities) < page_size:
break
page += 1
# Safety break to prevent infinite loops in case of API quirks
if page > 100:
print("Warning: Reached max page limit.")
break
except requests.exceptions.HTTPError as e:
if response.status_code == 400:
print("Bad Request. Check query parameters.")
elif response.status_code == 401:
print("Token expired or invalid. Refreshing...")
auth.access_token = None # Force refresh
continue
else:
raise e
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
break
return all_agents
Step 3: Diagnose the “Empty Array” Root Cause
Now we combine the authentication and retrieval logic into a diagnostic function. This function checks for the specific conditions that lead to an empty array.
def diagnose_empty_states(client_id: str, client_secret: str, subdomain: str):
"""
Main diagnostic function to identify why /agents/states returns empty.
"""
auth = CXoneAuth(client_id, client_secret, subdomain)
print(f"Attempting to connect to {subdomain}...")
# Step 1: Verify Auth and Scope
try:
token = auth.get_token()
print("OAuth Token acquired successfully.")
except ValueError as e:
print(f"Authentication Failed: {e}")
return
# Step 2: Verify Endpoint Reachability
if not verify_connection(auth, ""):
print("Connection verification failed.")
return
# Step 3: Retrieve Agent States
print("Fetching agent states...")
agents = get_all_agent_states(auth)
if len(agents) == 0:
print("RESULT: Empty Array Returned.")
print("Possible Causes:")
print("1. No agents are currently logged in to the NICE CXone UI.")
print("2. The OAuth client lacks the 'read:agent' scope.")
print("3. You are querying the wrong subdomain (e.g., test vs prod).")
print("4. The agents are logged in but not assigned to any skill (check agent configuration).")
else:
print(f"RESULT: Found {len(agents)} logged-in agents.")
for agent in agents:
print(f"- Agent ID: {agent.get('id')}, Name: {agent.get('name')}, State: {agent.get('state', {}).get('name', 'Unknown')}")
Complete Working Example
This script can be run directly. Replace the placeholder values with your actual NICE CXone credentials.
import requests
import time
import json
class CXoneAuth:
def __init__(self, client_id: str, client_secret: str, subdomain: str):
self.client_id = client_id
self.client_secret = client_secret
self.subdomain = subdomain
self.token_url = "https://login.nicecxone.com/oauth2/token"
self.api_base_url = f"https://{subdomain}.api.nicecxone.com"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry:
return self.access_token
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "read:agent"
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
try:
response = requests.post(self.token_url, data=payload, headers=headers)
response.raise_for_status()
data = response.json()
self.access_token = data["access_token"]
self.token_expiry = time.time() + (55 * 60)
return self.access_token
except requests.exceptions.HTTPError as e:
if response.status_code == 400:
raise ValueError("Invalid client credentials or grant type.")
elif response.status_code == 401:
raise ValueError("Unauthorized. Check Client ID and Secret.")
else:
raise Exception(f"OAuth Error: {e.response.text}")
def get_headers(self) -> dict:
token = self.get_token()
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
def get_all_agent_states(auth: CXoneAuth, page_size: int = 100) -> list:
all_agents = []
page = 1
url = f"{auth.api_base_url}/api/v2/agents/states"
headers = auth.get_headers()
while True:
params = {
"page_size": page_size,
"page": page
}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
entities = data.get("entities", [])
if not entities:
break
all_agents.extend(entities)
if len(entities) < page_size:
break
page += 1
if page > 100:
break
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
auth.access_token = None
continue
else:
raise e
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
break
return all_agents
def diagnose_empty_states(client_id: str, client_secret: str, subdomain: str):
auth = CXoneAuth(client_id, client_secret, subdomain)
print(f"Attempting to connect to {subdomain}...")
try:
token = auth.get_token()
print("OAuth Token acquired successfully.")
except ValueError as e:
print(f"Authentication Failed: {e}")
return
url = f"{auth.api_base_url}/api/v2/agents/states"
headers = auth.get_headers()
try:
response = requests.get(url, headers=headers)
if response.status_code == 401:
print("ERROR: 401 Unauthorized. Check OAuth scopes.")
return
elif response.status_code == 403:
print("ERROR: 403 Forbidden. Client lacks 'read:agent' scope.")
return
elif response.status_code == 404:
print("ERROR: 404 Not Found. Subdomain may be incorrect.")
return
elif response.status_code == 200:
data = response.json()
entities = data.get("entities", [])
if len(entities) == 0:
print("RESULT: Empty Array Returned.")
print("Diagnostic Checklist:")
print("1. Ensure at least one agent is LOGGED IN via the CXone UI.")
print("2. Verify the subdomain matches the logged-in environment.")
print("3. Confirm the OAuth Client has the 'read:agent' scope.")
else:
print(f"RESULT: Found {len(entities)} logged-in agents.")
for agent in entities[:5]: # Show first 5
state_name = agent.get('state', {}).get('name', 'Unknown')
print(f"- Agent: {agent.get('name')} (ID: {agent.get('id')}) -> State: {state_name}")
else:
print(f"Unexpected Status Code: {response.status_code}")
print(response.text)
except requests.exceptions.RequestException as e:
print(f"Network Error: {e}")
if __name__ == "__main__":
# REPLACE THESE VALUES WITH YOUR CREDENTIALS
CLIENT_ID = "your_client_id_here"
CLIENT_SECRET = "your_client_secret_here"
SUBDOMAIN = "your_subdomain_here" # e.g., "acme" for acme.nicecxone.com
if CLIENT_ID == "your_client_id_here":
print("Please update the CLIENT_ID, CLIENT_SECRET, and SUBDOMAIN variables.")
else:
diagnose_empty_states(CLIENT_ID, CLIENT_SECRET, SUBDOMAIN)
Common Errors & Debugging
Error: 403 Forbidden
- Cause: The OAuth client used to generate the token does not have the
read:agentscope assigned. The defaultread:profilescope is not sufficient for agent state data. - Fix: Go to the NICE CXone Admin Console, navigate to Security > OAuth Clients, edit your client, and add
read:agentto the scopes list. Regenerate the token.
Error: 200 OK with Empty entities Array
- Cause: The API call succeeded, but no agents are currently logged in. The
/agents/statesendpoint strictly returns agents with an active session. It does not return agents who are “Available” but not logged in, nor does it return offline agents. - Fix: Log into the NICE CXone Agent Desktop as a test user. Ensure the agent is in a “Logged In” state (not just “Available” in the UI, but authenticated). Run the script again.
Error: 401 Unauthorized
- Cause: The OAuth token has expired or is invalid.
- Fix: Ensure your token refresh logic is active. In the provided code, the
get_tokenmethod checks the expiry timestamp. If you are caching tokens manually, ensure you invalidate them after 60 minutes.
Error: 404 Not Found
- Cause: The subdomain in the URL is incorrect. For example, using
https://acme.api.nicecxone.comwhen the actual environment ishttps://acme-test.api.nicecxone.com. - Fix: Verify the subdomain string. It must match the prefix of your agent desktop URL (e.g.,
https://acme.nicecxone.com/agent-desktop).