Debugging Empty Agent States in NICE CXone: Why Your Agent Is Missing
What You Will Build
- A diagnostic script that queries the NICE CXone Agent States API and validates why a specific agent ID is returning an empty array or missing status.
- A working Python implementation using the
requestslibrary to handle OAuth2 authentication, API pagination, and state filtering. - A JavaScript/Node.js implementation using
axiosfor developers building server-side integrations.
Prerequisites
- NICE CXone API Client: You need a Client ID and Client Secret generated in the CXone Admin Console under API > Client Credentials.
- Required Scopes: The client must have the
read:agentscope. For full state visibility,read:agent:statusis often required depending on your tenant configuration. - Python 3.8+ or Node.js 16+.
- Dependencies:
- Python:
requests,pyjwt(optional, for token parsing). - Node.js:
axios,dotenv.
- Python:
Authentication Setup
NICE CXone uses standard OAuth2 Client Credentials flow. You cannot call the Agent States endpoint without a valid access token. The token expires after 15 minutes (900 seconds), so your code must handle refreshing.
Python Authentication Helper
This helper function fetches a token and caches it in memory. In production, you would store this in Redis or a secure vault.
import requests
import time
from typing import Optional
class CXoneAuth:
def __init__(self, client_id: str, client_secret: str, region: str = "us"):
self.client_id = client_id
self.client_secret = client_secret
self.region = region
self.token: Optional[str] = None
self.token_expiry: float = 0
# Determine base URL based on region
if region == "us":
self.auth_url = "https://platform.nicecxone.com/oauth/token"
self.base_url = "https://platform.nicecxone.com"
elif region == "eu":
self.auth_url = "https://platform.nicecxone-eu.com/oauth/token"
self.base_url = "https://platform.nicecxone-eu.com"
else:
raise ValueError("Unsupported region. Use 'us' or 'eu'.")
def get_token(self) -> str:
# Return cached token if valid
if self.token and time.time() < self.token_expiry - 60:
return self.token
# Fetch new token
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "read:agent read:agent:status"
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
response = requests.post(self.auth_url, data=payload, headers=headers)
if response.status_code != 200:
raise Exception(f"Auth Failed: {response.status_code} - {response.text}")
data = response.json()
self.token = data["access_token"]
self.token_expiry = time.time() + data["expires_in"]
return self.token
def get_headers(self) -> dict:
return {
"Authorization": f"Bearer {self.get_token()}",
"Content-Type": "application/json"
}
JavaScript Authentication Helper
const axios = require('axios');
class CXoneAuth {
constructor(clientId, clientSecret, region = 'us') {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.region = region;
this.token = null;
this.tokenExpiry = 0;
// Determine base URL based on region
if (region === 'us') {
this.authUrl = 'https://platform.nicecxone.com/oauth/token';
this.baseUrl = 'https://platform.nicecxone.com';
} else if (region === 'eu') {
this.authUrl = 'https://platform.nicecxone-eu.com/oauth/token';
this.baseUrl = 'https://platform.nicecxone-eu.com';
} else {
throw new Error('Unsupported region. Use us or eu.');
}
}
async getToken() {
// Return cached token if valid (with 60s buffer)
if (this.token && Date.now() < (this.tokenExpiry - 60000)) {
return this.token;
}
const payload = new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
scope: 'read:agent read:agent:status'
});
const response = await axios.post(this.authUrl, payload, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
this.token = response.data.access_token;
this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
return this.token;
}
getHeaders() {
return {
Authorization: `Bearer ${this.getToken()}`,
'Content-Type': 'application/json'
};
}
}
module.exports = CXoneAuth;
Implementation
Step 1: Querying Agent States Correctly
The endpoint GET /api/v2/agents/states does not return all agents in the system by default. It returns a paginated list of currently logged-in agents. If you pass an agentId query parameter, it returns only that agent’s state if they are logged in.
If the agent is logged out, the API returns an empty array []. This is the root cause of 90% of “missing agent” bugs.
Python: Fetching Specific Agent State
import requests
def get_agent_state(auth: CXoneAuth, agent_id: str) -> dict:
"""
Fetches the current state of a specific agent.
Returns an empty dict if the agent is not found or logged out.
"""
url = f"{auth.base_url}/api/v2/agents/states"
# The agentId parameter is critical here
params = {
"agentId": agent_id
}
headers = auth.get_headers()
try:
response = requests.get(url, headers=headers, params=params, timeout=10)
# 200 OK is expected even if the array is empty
if response.status_code == 200:
data = response.json()
print(f"Response Status: {response.status_code}")
print(f"Response Body: {data}")
# The API returns a list of state objects
if len(data) == 0:
print("WARNING: Agent is not currently logged in or state is not available.")
return {}
return data[0]
elif response.status_code == 401:
print("ERROR: Unauthorized. Check your OAuth token and scopes.")
return {}
elif response.status_code == 403:
print("ERROR: Forbidden. Your client lacks read:agent scope.")
return {}
elif response.status_code == 404:
print("ERROR: Agent not found. Verify the Agent ID exists in the system.")
return {}
else:
print(f"ERROR: Unexpected status {response.status_code}")
print(response.text)
return {}
except requests.exceptions.RequestException as e:
print(f"Network Error: {e}")
return {}
JavaScript: Fetching Specific Agent State
const CXoneAuth = require('./cxoneAuth'); // Assuming the class above is saved here
async function getAgentState(auth, agentId) {
const url = `${auth.baseUrl}/api/v2/agents/states`;
try {
const response = await axios.get(url, {
headers: auth.getHeaders(),
params: {
agentId: agentId
}
});
console.log(`Response Status: ${response.status}`);
console.log('Response Body:', response.data);
// The API returns an array
if (response.data.length === 0) {
console.warn('WARNING: Agent is not currently logged in or state is not available.');
return null;
}
return response.data[0];
} catch (error) {
if (error.response) {
if (error.response.status === 401) {
console.error('ERROR: Unauthorized. Check OAuth token and scopes.');
} else if (error.response.status === 403) {
console.error('ERROR: Forbidden. Client lacks read:agent scope.');
} else if (error.response.status === 404) {
console.error('ERROR: Agent not found. Verify Agent ID.');
} else {
console.error(`ERROR: Unexpected status ${error.response.status}`);
}
} else {
console.error('Network Error:', error.message);
}
return null;
}
}
Step 2: Diagnosing “Logged Out” vs “Not Found”
When GET /agents/states?agentId=123 returns [], you must determine if the agent exists but is offline, or if the ID is invalid. You do this by cross-referencing with the Agent Profile API.
The endpoint GET /api/v2/agents/{agentId} returns the agent’s configuration regardless of login status.
Python: Cross-Reference Check
def diagnose_agent_issue(auth: CXoneAuth, agent_id: str) -> None:
"""
Determines if an agent is missing from states due to being offline or non-existent.
"""
print(f"--- Diagnosing Agent ID: {agent_id} ---")
# 1. Check Agent Profile (Existence Check)
profile_url = f"{auth.base_url}/api/v2/agents/{agent_id}"
headers = auth.get_headers()
profile_response = requests.get(profile_url, headers=headers, timeout=10)
if profile_response.status_code == 404:
print("RESULT: Agent does not exist in the system.")
print("ACTION: Verify the Agent ID. It may be deleted or typed incorrectly.")
return
elif profile_response.status_code != 200:
print(f"RESULT: Error fetching profile: {profile_response.status_code}")
return
agent_profile = profile_response.json()
print(f"AGENT FOUND: Name = {agent_profile.get('name', 'Unknown')}")
print(f"AGENT EMAIL: {agent_profile.get('email', 'Unknown')}")
# 2. Check Agent State (Login Check)
state = get_agent_state(auth, agent_id)
if state:
print("RESULT: Agent is LOGGED IN.")
print(f"CURRENT STATE: {state.get('state', {}).get('name', 'Unknown')}")
print(f"STATE GROUP: {state.get('stateGroup', {}).get('name', 'Unknown')}")
else:
print("RESULT: Agent is LOGGED OUT.")
print("ACTION: The agent must log in via the Agent Desktop for the state API to return data.")
print("NOTE: Historical state data is only available via the Analytics API, not the real-time States API.")
Step 3: Handling Pagination for Bulk Checks
If you need to check multiple agents, do not loop through individual agentId calls. Use the list endpoint GET /api/v2/agents/states without an agentId parameter. This returns all currently online agents.
You can then filter this list in your code.
Python: Fetching All Online Agents
def get_all_online_agents(auth: CXoneAuth) -> list:
"""
Fetches all currently logged-in agents with pagination handling.
"""
url = f"{auth.base_url}/api/v2/agents/states"
headers = auth.get_headers()
all_agents = []
page_size = 25 # Max page size is often 25 or 100 depending on tenant settings
page = 1
while True:
params = {
"pageSize": page_size,
"page": page
}
response = requests.get(url, headers=headers, params=params, timeout=10)
if response.status_code != 200:
print(f"Error fetching page {page}: {response.status_code}")
break
data = response.json()
if not data:
break
all_agents.extend(data)
# Check for next page
# The response usually includes a 'nextPage' token or we rely on empty results
# CXone API typically returns an array. If len(data) < page_size, we are done.
if len(data) < page_size:
break
page += 1
print(f"Total online agents found: {len(all_agents)}")
return all_agents
Complete Working Example
This Python script combines authentication, diagnosis, and bulk checking into a single runnable module.
import requests
import time
import sys
import os
from typing import Optional, Dict, List
# --- Configuration ---
CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
REGION = os.getenv("CXONE_REGION", "us")
TARGET_AGENT_ID = os.getenv("TARGET_AGENT_ID", "12345") # Replace with your test agent ID
# --- Auth Class ---
class CXoneAuth:
def __init__(self, client_id: str, client_secret: str, region: str = "us"):
self.client_id = client_id
self.client_secret = client_secret
self.region = region
self.token: Optional[str] = None
self.token_expiry: float = 0
if region == "us":
self.auth_url = "https://platform.nicecxone.com/oauth/token"
self.base_url = "https://platform.nicecxone.com"
elif region == "eu":
self.auth_url = "https://platform.nicecxone-eu.com/oauth/token"
self.base_url = "https://platform.nicecxone-eu.com"
else:
raise ValueError("Unsupported region.")
def get_token(self) -> str:
if self.token and time.time() < self.token_expiry - 60:
return self.token
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "read:agent read:agent:status"
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(self.auth_url, data=payload, headers=headers)
if response.status_code != 200:
raise Exception(f"Auth Failed: {response.status_code} - {response.text}")
data = response.json()
self.token = data["access_token"]
self.token_expiry = time.time() + data["expires_in"]
return self.token
def get_headers(self) -> dict:
return {
"Authorization": f"Bearer {self.get_token()}",
"Content-Type": "application/json"
}
# --- Business Logic ---
def check_agent_status(auth: CXoneAuth, agent_id: str) -> Dict:
"""
Checks if an agent is online and returns their state.
"""
url = f"{auth.base_url}/api/v2/agents/states"
params = {"agentId": agent_id}
headers = auth.get_headers()
response = requests.get(url, headers=headers, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
if data:
return {"status": "online", "state": data[0]}
else:
return {"status": "offline"}
else:
return {"status": "error", "code": response.status_code, "message": response.text}
def verify_agent_exists(auth: CXoneAuth, agent_id: str) -> bool:
"""
Verifies if the agent ID exists in the system.
"""
url = f"{auth.base_url}/api/v2/agents/{agent_id}"
headers = auth.get_headers()
response = requests.get(url, headers=headers, timeout=10)
return response.status_code == 200
def main():
if not CLIENT_ID or not CLIENT_SECRET:
print("Error: CXONE_CLIENT_ID and CXONE_CLIENT_SECRET environment variables are required.")
sys.exit(1)
try:
auth = CXoneAuth(CLIENT_ID, CLIENT_SECRET, REGION)
print(f"Checking Agent ID: {TARGET_AGENT_ID}")
print("-" * 30)
# Step 1: Verify Existence
exists = verify_agent_exists(auth, TARGET_AGENT_ID)
if not exists:
print("1. Agent Existence: NOT FOUND")
print(" Action: Check the Agent ID in the Admin Console.")
return
else:
print("1. Agent Existence: FOUND")
# Step 2: Check Login Status
status_result = check_agent_status(auth, TARGET_AGENT_ID)
if status_result["status"] == "online":
print("2. Login Status: ONLINE")
state_info = status_result["state"]
print(f" Current State: {state_info.get('state', {}).get('name', 'N/A')}")
print(f" State Group: {state_info.get('stateGroup', {}).get('name', 'N/A')}")
print(f" Last Updated: {state_info.get('lastUpdated', 'N/A')}")
elif status_result["status"] == "offline":
print("2. Login Status: OFFLINE")
print(" Reason: The agent is not currently logged in via Agent Desktop.")
print(" Note: The /agents/states API only returns data for logged-in agents.")
else:
print(f"2. Error: {status_result['code']} - {status_result['message']}")
except Exception as e:
print(f"Fatal Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is expired, invalid, or the Client Secret is incorrect.
- Fix: Ensure your
CXoneAuthclass is refreshing the token. Check that the Client ID/Secret matches the one in the CXone Admin Console. Verify thescopein the token request includesread:agent.
Error: 403 Forbidden
- Cause: The OAuth client does not have the required permissions.
- Fix: Go to Admin > API > Client Credentials. Select your client. Ensure the scope
read:agentorread:agent:statusis added. Save and regenerate the token.
Error: Empty Array [] for Logged-In Agent
- Cause 1: The agent is logged into a different environment (e.g., Sandbox vs. Production).
- Fix: Verify the region (
usvseu) and the environment URL. Ensure the agent ID matches the environment.
- Fix: Verify the region (
- Cause 2: The agent is in a “Hidden” or “Maintenance” state that your client cannot see.
- Fix: Check if the agent is part of a team or group that your API client has restricted access to. Ensure the client has global read access or is scoped to the correct team.
- Cause 3: Caching Delay.
- Fix: The state API is near real-time but may have a 1-5 second latency. If you just logged in, wait a few seconds and retry.
Error: 404 Not Found on /api/v2/agents/{id}
- Cause: The Agent ID provided does not exist in the tenant.
- Fix: Copy the Agent ID directly from the Admin Console URL when viewing the agent’s profile. Do not use the agent’s email or name.