Debugging Empty Agent States in NICE CXone: Why Your GET /agents/states Returns []

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 requests library and TypeScript using the axios library.

Prerequisites

  • OAuth client credentials (Client ID and Client Secret) with the Agent-Read scope.
  • NICE CXone API Base URL (e.g., https://api-us-02.niceincontact.com).
  • Python 3.9+ with requests installed (pip install requests).
  • Node.js 16+ with axios installed (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-Read or Agent-Write scope.
  • 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 agentId UUID is invalid or does not exist in the current organization.
  • Fix: Verify the Agent ID. You can list all agents using GET /api/v2/agents to 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 stateId provided in the login payload is not a valid “Logged In” state for that skill, or the activityId is invalid.
  • Fix: Query GET /api/v2/skills/{skillId}/states to find the correct State ID that represents “Logged In”. Query GET /api/v2/activities to find a valid Activity ID (e.g., “Available”).

Official References