Bulk-Update NICE CXone Agent Skill Proficiencies via REST API
What You Will Build
- A Python script that authenticates to the NICE CXone platform and updates the skill proficiency levels for multiple agents in a single transaction.
- This tutorial uses the NICE CXone Admin API, specifically the
PUT /api/v2/admin/usersendpoint with a bulk payload structure. - The implementation is written in Python 3.9+ using the
requestslibrary for HTTP communication.
Prerequisites
- OAuth Client Type: You need an OAuth 2.0 Client ID and Client Secret. The client must have the Admin API access type enabled in the NICE CXone Admin portal.
- Required Scopes: The token must include the
admin:users:writescope to modify user properties. For reading current proficiency states (optional but recommended for validation), includeadmin:users:read. - SDK/API Version: NICE CXone Admin API v2.
- Language/Runtime: Python 3.9 or higher.
- External Dependencies:
requests: For HTTP handling.python-dotenv: For secure credential management.
Install the dependencies using pip:
pip install requests python-dotenv
Authentication Setup
NICE CXone uses standard OAuth 2.0 Client Credentials flow for server-to-server API access. You must exchange your Client ID and Client Secret for an access token before making any API calls.
Create a file named .env in your project root to store credentials securely:
CXONE_CLIENT_ID=your_client_id_here
CXONE_CLIENT_SECRET=your_client_secret_here
CXONE_ENVIRONMENT=us-02 # or eu-01, ap-01, etc.
The following code demonstrates how to retrieve the token. This function includes basic error handling for authentication failures.
import requests
import os
from dotenv import load_dotenv
load_dotenv()
def get_access_token() -> str:
"""
Retrieves an OAuth 2.0 access token from NICE CXone.
Returns:
str: The access token.
Raises:
requests.exceptions.HTTPError: If authentication fails.
"""
client_id = os.getenv("CXONE_CLIENT_ID")
client_secret = os.getenv("CXONE_CLIENT_SECRET")
environment = os.getenv("CXONE_ENVIRONMENT", "us-02")
if not client_id or not client_secret:
raise ValueError("CXONE_CLIENT_ID and CXONE_CLIENT_SECRET must be set in .env")
# Base URL depends on the environment
auth_url = f"https://platform.{environment}.niceincontact.com/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "admin:users:write admin:users:read"
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
response = requests.post(auth_url, data=payload, headers=headers)
if response.status_code != 200:
raise requests.exceptions.HTTPError(
f"Authentication failed with status {response.status_code}: {response.text}"
)
token_data = response.json()
return token_data["access_token"]
# Example usage
if __name__ == "__main__":
try:
token = get_access_token()
print("Token retrieved successfully.")
except Exception as e:
print(f"Error: {e}")
Implementation
Step 1: Identify Target Users and Skills
Before updating proficiencies, you must know the unique identifiers for the users and the skills. NICE CXone uses internal GUIDs for these entities.
While you can hardcode these IDs if you know them, a robust script typically fetches them first. For this tutorial, we assume you have a list of User IDs and Skill IDs. If you do not, you can retrieve them using the GET /api/v2/admin/users and GET /api/v2/admin/skills endpoints.
Here is a helper function to fetch a list of users by email, which is often easier to manage than GUIDs:
def get_user_ids_by_emails(emails: list[str], token: str, environment: str) -> list[str]:
"""
Fetches user IDs for a list of email addresses.
Args:
emails: List of user email addresses.
token: Valid OAuth access token.
environment: CXone environment (e.g., us-02).
Returns:
List of user GUIDs.
"""
base_url = f"https://platform.{environment}.niceincontact.com/api/v2/admin/users"
headers = {"Authorization": f"Bearer {token}"}
user_ids = []
# CXone API supports querying users. We construct a query string.
# Note: The admin API does not support direct email filtering in the query string
# for bulk lookups efficiently without pagination logic.
# For simplicity in this tutorial, we assume you already have the IDs.
# In production, you would iterate through pages of GET /api/v2/admin/users?pageSize=100
# and match emails.
print("Warning: This helper is a placeholder. In production, implement pagination to fetch User IDs by email.")
return user_ids
Step 2: Construct the Bulk Update Payload
The core of this operation is the PUT /api/v2/admin/users endpoint. Unlike some APIs that offer a dedicated “update skills” endpoint, CXone requires you to send the full user object or a partial update object containing the skills array.
The skills array contains objects with skillId and proficiency fields. The proficiency field is an integer ranging from 1 to 10.
Critical Note: When performing a bulk update via PUT, you are replacing the existing skill set for those users unless you explicitly merge the data client-side. NICE CXone’s PUT /api/v2/admin/users endpoint is designed to update specific fields if you use a PATCH-like behavior, but the documentation specifies that PUT on the collection endpoint often expects the full resource representation or specific fields to be updated. However, for skills, it is safest to send only the skills array within the user object for the users you intend to modify.
The payload structure for a single user update looks like this:
{
"id": "user-guid-here",
"skills": [
{
"skillId": "skill-guid-1",
"proficiency": 8
},
{
"skillId": "skill-guid-2",
"proficiency": 5
}
]
}
For bulk operations, you send an array of these objects.
Step 3: Execute the Bulk Update
The following function performs the actual API call. It includes retry logic for 429 (Too Many Requests) responses, which are common when processing large batches.
import time
import json
def bulk_update_agent_skills(
token: str,
environment: str,
user_skill_updates: list[dict],
max_retries: int = 3
) -> dict:
"""
Updates skill proficiencies for multiple agents.
Args:
token: Valid OAuth access token.
environment: CXone environment.
user_skill_updates: List of dicts, each containing 'id' and 'skills'.
max_retries: Number of retries for 429 errors.
Returns:
The JSON response from the API.
"""
base_url = f"https://platform.{environment}.niceincontact.com/api/v2/admin/users"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# The payload is the list of user objects to update
payload = user_skill_updates
retries = 0
while retries <= max_retries:
try:
response = requests.put(
base_url,
headers=headers,
json=payload,
timeout=30
)
if response.status_code == 200 or response.status_code == 204:
print(f"Success: Updated {len(user_skill_updates)} users.")
return response.json()
elif response.status_code == 429:
# Handle Rate Limiting
retry_after = int(response.headers.get("Retry-After", 5))
retries += 1
print(f"Rate limited. Retrying in {retry_after} seconds... (Attempt {retries}/{max_retries})")
time.sleep(retry_after)
continue
elif response.status_code == 400:
# Bad Request - likely malformed JSON or invalid GUIDs
raise ValueError(f"Bad Request: {response.text}")
elif response.status_code == 401:
raise PermissionError("Unauthorized. Token may be expired.")
elif response.status_code == 403:
raise PermissionError("Forbidden. Check OAuth scopes (admin:users:write).")
else:
raise requests.exceptions.HTTPError(
f"Unexpected status code {response.status_code}: {response.text}"
)
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
raise
raise RuntimeError("Max retries exceeded due to rate limiting.")
Step 4: Processing Results and Validation
After the update, it is good practice to verify that the changes were applied. The PUT response typically returns the updated user objects. You can inspect this data to confirm the proficiency values match your input.
If the API returns a 204 No Content, the update was successful but no body is returned. In this case, you may want to perform a subsequent GET request for a sample of users to validate.
Complete Working Example
This script combines authentication, payload construction, and execution into a single runnable module. It assumes you have a predefined list of updates.
import requests
import os
import time
import json
from dotenv import load_dotenv
load_dotenv()
def get_access_token() -> str:
"""
Retrieves an OAuth 2.0 access token from NICE CXone.
"""
client_id = os.getenv("CXONE_CLIENT_ID")
client_secret = os.getenv("CXONE_CLIENT_SECRET")
environment = os.getenv("CXONE_ENVIRONMENT", "us-02")
if not client_id or not client_secret:
raise ValueError("CXONE_CLIENT_ID and CXONE_CLIENT_SECRET must be set in .env")
auth_url = f"https://platform.{environment}.niceincontact.com/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "admin:users:write admin:users:read"
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(auth_url, data=payload, headers=headers)
if response.status_code != 200:
raise requests.exceptions.HTTPError(f"Auth failed: {response.text}")
return response.json()["access_token"]
def bulk_update_agent_skills(token: str, environment: str, updates: list[dict]) -> dict:
"""
Performs the bulk update of user skills.
"""
base_url = f"https://platform.{environment}.niceincontact.com/api/v2/admin/users"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
max_retries = 3
retries = 0
while retries <= max_retries:
try:
response = requests.put(base_url, headers=headers, json=updates, timeout=30)
if response.status_code in [200, 204]:
print("Bulk update successful.")
if response.status_code == 200:
return response.json()
return {}
elif response.status_code == 429:
wait_time = int(response.headers.get("Retry-After", 5))
retries += 1
print(f"Rate limited. Waiting {wait_time}s. Retry {retries}/{max_retries}")
time.sleep(wait_time)
continue
else:
print(f"Error: {response.status_code} - {response.text}")
raise requests.exceptions.HTTPError(f"API Error: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
raise
raise RuntimeError("Max retries exceeded.")
def main():
try:
# 1. Authenticate
print("Authenticating...")
token = get_access_token()
environment = os.getenv("CXONE_ENVIRONMENT", "us-02")
# 2. Define Updates
# Replace these GUIDs with real IDs from your CXone instance.
# You can find User IDs in the Admin Console or via GET /api/v2/admin/users
# You can find Skill IDs in the Admin Console or via GET /api/v2/admin/skills
updates = [
{
"id": "00000000-0000-0000-0000-000000000001", # User 1 ID
"skills": [
{
"skillId": "11111111-1111-1111-1111-111111111111", # Skill A ID
"proficiency": 9
},
{
"skillId": "22222222-2222-2222-2222-222222222222", # Skill B ID
"proficiency": 7
}
]
},
{
"id": "00000000-0000-0000-0000-000000000002", # User 2 ID
"skills": [
{
"skillId": "11111111-1111-1111-1111-111111111111", # Skill A ID
"proficiency": 5
}
]
}
]
print(f"Preparing to update {len(updates)} users...")
# 3. Execute Update
result = bulk_update_agent_skills(token, environment, updates)
if result:
print("Updated Users Response:")
print(json.dumps(result, indent=2))
else:
print("Update complete (No Content returned).")
except Exception as e:
print(f"Critical Error: {e}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
Cause: The OAuth token lacks the necessary scope.
Fix: Ensure your scope parameter in the token request includes admin:users:write. If you are using a custom OAuth client, verify that the “Admin API” permission is granted in the CXone Admin Console under Settings > Integrations > OAuth.
Error: 400 Bad Request
Cause: Invalid GUID format, non-existent User ID, or non-existent Skill ID.
Fix: Validate the id and skillId fields. They must be valid UUIDs (GUIDs). Ensure the skill exists in the environment. You can verify skill existence by calling GET /api/v2/admin/skills/{skillId}.
Error: 429 Too Many Requests
Cause: You are sending too many requests in a short period, or the batch size is too large.
Fix: Implement exponential backoff. The code above includes a basic retry loop. For very large batches (1000+ users), split the updates list into chunks of 50-100 users and process them sequentially with a small delay between chunks.
Error: Skill Not Found
Cause: The skillId provided does not exist in the specified environment.
Fix: Use the GET /api/v2/admin/skills endpoint to retrieve the correct GUID for the skill name.