Debugging Empty Agent States in NICE CXone: Why Your GET /agents/states Returns
What You Will Build
- One sentence: This tutorial provides a working diagnostic script to query NICE CXone Agent State APIs and identify why an agent appears offline or returns an empty array.
- One sentence: This uses the NICE CXone REST API v2 endpoints for Agent State and Agent Activity.
- One sentence: The implementation is covered in Python using the
requestslibrary and TypeScript using theaxioslibrary.
Prerequisites
- OAuth client credentials (Client ID and Client Secret) with the
Agent-Readscope. - NICE CXone API Base URL (e.g.,
https://api-us-02.niceincontact.com). - Python 3.9+ with
requestsinstalled (pip install requests). - Node.js 16+ with
axiosinstalled (npm install axios). - A valid Agent ID (UUID format) from your CXone environment.
Authentication Setup
Before querying agent states, you must obtain a valid Bearer token. NICE CXone uses OAuth 2.0 Client Credentials Grant. If your token is expired or lacks the correct scope, the API will return 401 or 403 errors, which can sometimes be misinterpreted as empty data if error handling is loose.
Python Authentication Helper
import requests
import time
from typing import Optional
class CXoneAuth:
def __init__(self, base_url: str, client_id: str, client_secret: str):
self.base_url = base_url.rstrip('/')
self.client_id = client_id
self.client_secret = client_secret
self.token = None
self.token_expiry = 0
def get_token(self) -> str:
"""
Retrieves an OAuth2 token. Caches it until expiration.
"""
# If we have a token and it is not expired, return it
if self.token and time.time() < self.token_expiry:
return self.token
url = f"{self.base_url}/oauth/token"
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(url, data=data, timeout=10)
response.raise_for_status()
token_data = response.json()
self.token = token_data['access_token']
# Subtract 60 seconds to ensure we refresh before hard expiry
self.token_expiry = time.time() + (token_data['expires_in'] - 60)
return self.token
except requests.exceptions.HTTPError as e:
print(f"Authentication failed: {e.response.status_code} - {e.response.text}")
raise
except Exception as e:
print(f"Network error during authentication: {e}")
raise
# Usage Example
# auth = CXoneAuth("https://api-us-02.niceincontact.com", "YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET")
# token = auth.get_token()
TypeScript Authentication Helper
import axios from 'axios';
interface TokenResponse {
access_token: string;
expires_in: number;
}
class CXoneAuth {
private base_url: string;
private client_id: string;
private client_secret: string;
private token: string | null = null;
private token_expiry: number = 0;
constructor(base_url: string, client_id: string, client_secret: string) {
this.base_url = base_url.replace(/\/$/, '');
this.client_id = client_id;
this.client_secret = client_secret;
}
async getToken(): Promise<string> {
// Return cached token if valid
if (this.token && Date.now() < this.token_expiry) {
return this.token;
}
const url = `${this.base_url}/oauth/token`;
const data = new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.client_id,
client_secret: this.client_secret
});
try {
const response = await axios.post<TokenResponse>(url, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
this.token = response.data.access_token;
// Subtract 60 seconds for safety margin
this.token_expiry = Date.now() + (response.data.expires_in - 60) * 1000;
return this.token;
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
console.error(`Auth Failed: ${error.response.status} - ${error.response.data}`);
} else {
console.error('Auth Network Error:', error);
}
throw error;
}
}
}
Implementation
The core issue described in the topic is that GET /agents/states returns an empty array []. In NICE CXone, an agent must be explicitly logged in to a specific Skill or Interaction Type to appear in the state list. If an agent is authenticated in the system but has not performed a “Login” action via the API or UI, they have no active state, and the query returns empty.
Step 1: Query Current Agent States
First, we verify the current state. We will query the agent state for a specific Agent ID. If this returns an empty list, it confirms the agent is not logged in to any skill.
Endpoint: GET /api/v2/agents/{agentId}/states
Scope: Agent-Read
Python Implementation
import requests
from typing import List, Dict, Any
def get_agent_states(auth: CXoneAuth, agent_id: str) -> List[Dict[str, Any]]:
"""
Retrieves the current login states for a specific agent.
Returns an empty list if the agent is not logged in to any skill.
"""
url = f"{auth.base_url}/api/v2/agents/{agent_id}/states"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
states = data.get('states', [])
print(f"Found {len(states)} active state(s) for Agent {agent_id}")
return states
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
print(f"Agent ID {agent_id} not found. Check the UUID.")
elif e.response.status_code == 403:
print("Access Denied. Ensure the OAuth token has 'Agent-Read' scope.")
else:
print(f"HTTP Error: {e.response.status_code} - {e.response.text}")
return []
except Exception as e:
print(f"Error fetching states: {e}")
return []
# Example Usage
# states = get_agent_states(auth, "00000000-0000-0000-0000-000000000000")
TypeScript Implementation
interface AgentState {
agentId: string;
stateId: string;
skillId: string;
loginTime: string;
activityId: string;
}
async function getAgentStates(auth: CXoneAuth, agentId: string): Promise<AgentState[]> {
const url = `${auth.base_url}/api/v2/agents/${agentId}/states`;
const token = await auth.getToken();
try {
const response = await axios.get<{ states: AgentState[] }>(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const states = response.data.states || [];
console.log(`Found ${states.length} active state(s) for Agent ${agentId}`);
return states;
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 404) {
console.error(`Agent ID ${agentId} not found.`);
} else if (error.response.status === 403) {
console.error("Access Denied. Check OAuth scopes.");
} else {
console.error(`HTTP Error: ${error.response.status}`);
}
} else {
console.error('Error fetching states:', error);
}
return [];
}
}
Step 2: Diagnose the “Empty Array” Cause
If Step 1 returns [], the agent is not logged in. To fix this, you must log the agent in. However, before logging in, you must know which Skill to log into. You cannot log into a generic “agent” state; you must log into a specific Skill ID.
We will fetch the available Skills for the agent or a specific Skill ID if known.
Endpoint: GET /api/v2/skills
Scope: Skills-Read
Python: Fetch Available Skills
def get_available_skills(auth: CXoneAuth) -> List[Dict[str, Any]]:
"""
Fetches all available skills in the organization.
We need a valid skillId to perform the login.
"""
url = f"{auth.base_url}/api/v2/skills"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
skills = data.get('entities', [])
print(f"Found {len(skills)} skills.")
if skills:
print(f"First Skill ID: {skills[0]['id']}")
return skills
except Exception as e:
print(f"Error fetching skills: {e}")
return []
Step 3: Log the Agent In (The Fix)
To populate the /agents/states array, you must send a POST request to log the agent into a specific skill. This is the critical step that resolves the “empty array” issue.
Endpoint: POST /api/v2/agents/{agentId}/states
Scope: Agent-Write
Request Body:
{
"stateId": "00000000-0000-0000-0000-000000000000",
"skillId": "11111111-1111-1111-1111-111111111111",
"activityId": "22222222-2222-2222-2222-222222222222"
}
Note: stateId is often the ID of the “Logged In” state definition for that skill. activityId is the initial activity (e.g., “Available”, “Busy”). If you do not specify these, the API may use defaults, but it is safer to be explicit.
Python: Login Agent
def login_agent(auth: CXoneAuth, agent_id: str, skill_id: str, state_id: str, activity_id: str) -> bool:
"""
Logs an agent into a specific skill.
Args:
agent_id: UUID of the agent.
skill_id: UUID of the skill to log into.
state_id: UUID of the State Definition (usually 'Logged In').
activity_id: UUID of the initial Activity (e.g., 'Available').
"""
url = f"{auth.base_url}/api/v2/agents/{agent_id}/states"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
payload = {
"skillId": skill_id,
"stateId": state_id,
"activityId": activity_id
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
if response.status_code == 201:
print(f"Successfully logged in Agent {agent_id} to Skill {skill_id}")
return True
elif response.status_code == 409:
print(f"Agent is already logged into this skill or another conflicting skill.")
return False
else:
response.raise_for_status()
return False
except requests.exceptions.HTTPError as e:
print(f"Login Failed: {e.response.status_code} - {e.response.text}")
return False
except Exception as e:
print(f"Error logging in agent: {e}")
return False
TypeScript: Login Agent
async function loginAgent(
auth: CXoneAuth,
agentId: string,
skillId: string,
stateId: string,
activityId: string
): Promise<boolean> {
const url = `${auth.base_url}/api/v2/agents/${agentId}/states`;
const token = await auth.getToken();
const payload = {
skillId: skillId,
stateId: stateId,
activityId: activityId
};
try {
const response = await axios.post(url, payload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.status === 201) {
console.log(`Successfully logged in Agent ${agentId} to Skill ${skillId}`);
return true;
} else if (response.status === 409) {
console.log(`Agent is already logged into this skill.`);
return false;
}
return false;
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
console.error(`Login Failed: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
} else {
console.error('Error logging in agent:', error);
}
return false;
}
}
Step 4: Verify the State
After logging in, run the get_agent_states function again. The array should now contain one object with the skillId, stateId, and loginTime.
Complete Working Example
Below is a complete Python script that authenticates, checks for an empty state, logs the agent in if necessary, and verifies the result.
import requests
import time
import sys
from typing import List, Dict, Any, Optional
# Configuration
BASE_URL = "https://api-us-02.niceincontact.com" # Replace with your environment
CLIENT_ID = "YOUR_CLIENT_ID"
CLIENT_SECRET = "YOUR_CLIENT_SECRET"
AGENT_ID = "YOUR_AGENT_UUID"
# These IDs must be valid in your environment.
# You can find them via the Admin UI or by querying /api/v2/skills and /api/v2/activities
TARGET_SKILL_ID = "YOUR_SKILL_UUID"
TARGET_STATE_ID = "YOUR_STATE_UUID" # Usually the 'Logged In' state definition
TARGET_ACTIVITY_ID = "YOUR_ACTIVITY_UUID" # Usually 'Available'
class CXoneClient:
def __init__(self, base_url: str, client_id: str, client_secret: str):
self.base_url = base_url.rstrip('/')
self.client_id = client_id
self.client_secret = client_secret
self.token = None
self.token_expiry = 0
def _get_headers(self) -> Dict[str, str]:
return {
"Authorization": f"Bearer {self._get_token()}",
"Content-Type": "application/json"
}
def _get_token(self) -> str:
if self.token and time.time() < self.token_expiry:
return self.token
url = f"{self.base_url}/oauth/token"
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(url, data=data, timeout=10)
response.raise_for_status()
token_data = response.json()
self.token = token_data['access_token']
self.token_expiry = time.time() + (token_data['expires_in'] - 60)
return self.token
except Exception as e:
print(f"Authentication Error: {e}")
sys.exit(1)
def get_agent_states(self, agent_id: str) -> List[Dict[str, Any]]:
url = f"{self.base_url}/api/v2/agents/{agent_id}/states"
try:
response = requests.get(url, headers=self._get_headers(), timeout=10)
response.raise_for_status()
return response.json().get('states', [])
except requests.exceptions.HTTPError as e:
print(f"Error fetching states: {e.response.status_code}")
return []
def login_agent(self, agent_id: str, skill_id: str, state_id: str, activity_id: str) -> bool:
url = f"{self.base_url}/api/v2/agents/{agent_id}/states"
payload = {
"skillId": skill_id,
"stateId": state_id,
"activityId": activity_id
}
try:
response = requests.post(url, json=payload, headers=self._get_headers(), timeout=10)
if response.status_code == 201:
return True
elif response.status_code == 409:
print("Agent already logged in.")
return True
else:
print(f"Login failed: {response.status_code} - {response.text}")
return False
except Exception as e:
print(f"Exception during login: {e}")
return False
def main():
print(f"Starting CXone Agent State Diagnostic...")
client = CXoneClient(BASE_URL, CLIENT_ID, CLIENT_SECRET)
# Step 1: Check current state
print(f"Checking states for Agent {AGENT_ID}...")
current_states = client.get_agent_states(AGENT_ID)
if len(current_states) > 0:
print(f"Agent is already logged in. States: {current_states}")
return
print("Agent is NOT logged in. Initiating login...")
# Step 2: Log in the agent
success = client.login_agent(AGENT_ID, TARGET_SKILL_ID, TARGET_STATE_ID, TARGET_ACTIVITY_ID)
if success:
# Step 3: Verify
print("Login request accepted. Verifying state...")
time.sleep(1) # Brief delay for server propagation
new_states = client.get_agent_states(AGENT_ID)
if len(new_states) > 0:
print(f"Success! Agent is now logged in. State: {new_states[0]}")
else:
print("Warning: Login succeeded but state query still returns empty. Check server latency or skill configuration.")
else:
print("Failed to log in agent.")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is expired, invalid, or the Client ID/Secret is incorrect.
- Fix: Verify your credentials in the CXone Admin Console under Integrations > OAuth. Ensure your Python/JS script is fetching a fresh token.
Error: 403 Forbidden
- Cause: The OAuth Client does not have the
Agent-ReadorAgent-Writescope. - Fix: Go to CXone Admin > Integrations > OAuth Clients. Select your client and ensure “Agent” permissions are granted. Re-generate the token after changing scopes.
Error: 404 Not Found
- Cause: The
agentIdUUID is invalid or does not exist in the current organization. - Fix: Verify the Agent ID. You can list all agents using
GET /api/v2/agentsto find the correct UUID.
Error: 409 Conflict
- Cause: The agent is already logged into the specified skill, or they are logged into a different skill that conflicts (depending on routing rules).
- Fix: This is often expected behavior. If you want to switch skills, you must first log the agent out of the previous skill using
DELETE /api/v2/agents/{agentId}/states/{stateId}.
Error: Empty Array [] After Login
- Cause: The
stateIdprovided in the login payload is not a valid “Logged In” state for that skill, or theactivityIdis invalid. - Fix: Query
GET /api/v2/skills/{skillId}/statesto find the correct State ID that represents “Logged In”. QueryGET /api/v2/activitiesto find a valid Activity ID (e.g., “Available”).