How to Authenticate Against the NICE CXone API Using Client Credentials
What You Will Build
- A script that exchanges a Client ID and Client Secret for a valid OAuth 2.0 access token and refresh token.
- This uses the NICE CXone OAuth 2.0 Token Endpoint (
/api/v2/oauth/token). - The tutorial covers Python and JavaScript implementations.
Prerequisites
- OAuth Client Type: Confidential Client (Server-to-Server).
- Required Scopes: Determine the scopes needed for your downstream API calls (e.g.,
platform-user-read,analytics-query). For authentication itself, no specific scope is required in the token request, but you must request the scopes your application intends to use. - SDK Version: NICE CXone Python SDK (
nice-cxone-python-sdk) or raw HTTP requests usingrequests. - Language/Runtime: Python 3.8+ or Node.js 14+.
- External Dependencies:
- Python:
requests - JavaScript:
axios(optional, nativefetchis also sufficient)
- Python:
Authentication Setup
The NICE CXone platform uses standard OAuth 2.0. For server-to-server integrations where no human user is logging in, the Client Credentials Grant is the correct flow. This flow requires a Client ID and a Client Secret, which are generated in the NICE CXone Admin Console under Settings > Integrations > OAuth.
Unlike the Authorization Code Grant, this flow does not redirect a user to a login page. Instead, your application sends the credentials directly to the token endpoint. The response contains an access_token (valid for 1 hour by default) and a refresh_token (valid for 30 days by default).
Critical Security Note
Never hardcode Client IDs or Secrets in source code. Use environment variables or a secrets manager. The examples below use environment variables for demonstration.
Implementation
Step 1: Constructing the Token Request
The token endpoint is always located at the base URL of your CXone region followed by /api/v2/oauth/token. Common region bases include:
- Global:
https://api.cxone.com - EU:
https://api.eu.cxone.com - US Gov:
https://api.usgov.cxone.com
The request method is POST. The content type must be application/x-www-form-urlencoded.
Python Implementation
import os
import requests
from requests.exceptions import HTTPError
def get_cxone_token(base_url: str, client_id: str, client_secret: str, scope: str = "platform-user-read") -> dict:
"""
Authenticates with NICE CXone using Client Credentials Grant.
Args:
base_url: The CXone API base URL (e.g., https://api.cxone.com)
client_id: OAuth Client ID
client_secret: OAuth Client Secret
scope: Space-separated list of OAuth scopes
Returns:
Dictionary containing access_token, refresh_token, and expires_in
"""
token_url = f"{base_url}/api/v2/oauth/token"
# The body must be form-urlencoded, not JSON
payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": scope
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
try:
response = requests.post(token_url, data=payload, headers=headers)
response.raise_for_status()
return response.json()
except HTTPError as http_err:
print(f"HTTP error occurred: {http_err}")
# Log the response body for debugging 4xx errors
try:
print(f"Response body: {response.text}")
except:
pass
raise
except Exception as err:
print(f"An error occurred: {err}")
raise
if __name__ == "__main__":
# Load credentials from environment
BASE_URL = os.getenv("CXONE_BASE_URL", "https://api.cxone.com")
CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
if not CLIENT_ID or not CLIENT_SECRET:
raise ValueError("CXONE_CLIENT_ID and CXONE_CLIENT_SECRET must be set in environment")
token_data = get_cxone_token(BASE_URL, CLIENT_ID, CLIENT_SECRET, scope="platform-user-read analytics-query")
print(f"Access Token: {token_data['access_token'][:20]}...")
print(f"Expires In: {token_data['expires_in']} seconds")
JavaScript (Node.js) Implementation
const https = require('https');
const url = require('url');
/**
* Authenticates with NICE CXone using Client Credentials Grant.
*
* @param {string} baseUrl - The CXone API base URL (e.g., https://api.cxone.com)
* @param {string} clientId - OAuth Client ID
* @param {string} clientSecret - OAuth Client Secret
* @param {string} scope - Space-separated list of OAuth scopes
* @returns {Promise<Object>} - Token response object
*/
async function getCxoneToken(baseUrl, clientId, clientSecret, scope = 'platform-user-read') {
const tokenEndpoint = '/api/v2/oauth/token';
const fullUrl = new URL(tokenEndpoint, baseUrl);
// Construct form-urlencoded body
const formData = new URLSearchParams();
formData.append('grant_type', 'client_credentials');
formData.append('client_id', clientId);
formData.append('client_secret', clientSecret);
formData.append('scope', scope);
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(formData.toString())
}
};
return new Promise((resolve, reject) => {
const req = https.request(fullUrl, options, (res) => {
let data = '';
// Collect response data
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error('Failed to parse JSON response'));
}
} else {
reject(new Error(`HTTP Error: ${res.statusCode} - ${data}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
// Write data to request body
req.write(formData.toString());
req.end();
});
}
// Usage Example
(async () => {
const BASE_URL = process.env.CXONE_BASE_URL || 'https://api.cxone.com';
const CLIENT_ID = process.env.CXONE_CLIENT_ID;
const CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET;
if (!CLIENT_ID || !CLIENT_SECRET) {
throw new Error('CXONE_CLIENT_ID and CXONE_CLIENT_SECRET must be set in environment');
}
try {
const tokenData = await getCxoneToken(BASE_URL, CLIENT_ID, CLIENT_SECRET, 'platform-user-read analytics-query');
console.log(`Access Token: ${tokenData.access_token.substring(0, 20)}...`);
console.log(`Expires In: ${tokenData.expires_in} seconds`);
} catch (error) {
console.error('Authentication failed:', error.message);
}
})();
Step 2: Handling the Response and Refreshing Tokens
The response from /api/v2/oauth/token looks like this:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"expires_in": 3600,
"token_type": "Bearer"
}
The access_token is a JWT. You must include this token in the Authorization header of subsequent API requests as Bearer <access_token>.
The token expires after expires_in seconds (usually 3600 seconds / 1 hour). You should not make a new token request every time you call an API. Instead, cache the token and only request a new one when the current one is expired or near expiration.
Token Refresh Logic
When the access token expires, you can use the refresh_token to get a new access token without re-authenticating with the client secret. This uses the refresh_token grant type.
Python Refresh Example:
def refresh_cxone_token(base_url: str, refresh_token: str, client_id: str, client_secret: str) -> dict:
"""
Refreshes an expired CXone access token.
Args:
base_url: The CXone API base URL
refresh_token: The refresh token from the previous token response
client_id: OAuth Client ID
client_secret: OAuth Client Secret
Returns:
New token response dictionary
"""
token_url = f"{base_url}/api/v2/oauth/token"
payload = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": client_id,
"client_secret": client_secret
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
response = requests.post(token_url, data=payload, headers=headers)
response.raise_for_status()
return response.json()
Important: The refresh token is single-use. When you use it, it is consumed and a new refresh token is returned in the response. You must update your stored refresh token with the new one.
Step 3: Making an Authenticated API Call
Once you have the access token, you can call any CXone API. Here is an example of fetching user information.
Python:
def get_user_info(base_url: str, access_token: str, user_id: str) -> dict:
"""
Fetches user information from CXone.
Args:
base_url: The CXone API base URL
access_token: Valid OAuth access token
user_id: The ID of the user to fetch
Returns:
User object dictionary
"""
endpoint = f"{base_url}/api/v2/users/{user_id}"
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json"
}
response = requests.get(endpoint, headers=headers)
response.raise_for_status()
return response.json()
JavaScript:
async function getUserInfo(baseUrl, accessToken, userId) {
const endpoint = `/api/v2/users/${userId}`;
const fullUrl = new URL(endpoint, baseUrl);
const options = {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json'
}
};
return new Promise((resolve, reject) => {
const req = https.request(fullUrl, options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error('Failed to parse JSON response'));
}
} else {
reject(new Error(`HTTP Error: ${res.statusCode} - ${data}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.end();
});
}
Complete Working Example
Here is a complete Python script that handles authentication, token caching, and an API call.
import os
import time
import requests
from requests.exceptions import HTTPError
class CxoneClient:
def __init__(self, base_url: str, client_id: str, client_secret: str, scopes: list):
self.base_url = base_url.rstrip('/')
self.client_id = client_id
self.client_secret = client_secret
self.scopes = " ".join(scopes)
self.token = None
self.token_expiry = 0
def _get_token(self) -> dict:
"""Internal method to fetch a new token."""
token_url = f"{self.base_url}/api/v2/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": self.scopes
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(token_url, data=payload, headers=headers)
response.raise_for_status()
return response.json()
def _refresh_token(self, refresh_token: str) -> dict:
"""Internal method to refresh an existing token."""
token_url = f"{self.base_url}/api/v2/oauth/token"
payload = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": self.client_id,
"client_secret": self.client_secret
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(token_url, data=payload, headers=headers)
response.raise_for_status()
return response.json()
def ensure_token(self) -> str:
"""
Ensures a valid access token is available.
Refreshes if expired or not present.
"""
now = time.time()
# If token is missing or expired (with 5 minute buffer)
if not self.token or now >= (self.token_expiry - 300):
if self.token and 'refresh_token' in self.token:
# Try refreshing first
try:
new_token_data = self._refresh_token(self.token['refresh_token'])
self.token = new_token_data
except HTTPError:
# If refresh fails, fall back to re-authentication
print("Refresh token failed. Re-authenticating...")
self.token = self._get_token()
else:
# No refresh token, get new one
self.token = self._get_token()
# Update expiry time
self.token_expiry = now + self.token.get('expires_in', 3600)
return self.token['access_token']
def get_user(self, user_id: str) -> dict:
"""Fetches user details."""
access_token = self.ensure_token()
endpoint = f"{self.base_url}/api/v2/users/{user_id}"
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json"
}
response = requests.get(endpoint, headers=headers)
response.raise_for_status()
return response.json()
# Usage
if __name__ == "__main__":
BASE_URL = os.getenv("CXONE_BASE_URL", "https://api.cxone.com")
CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
USER_ID = os.getenv("CXONE_USER_ID", "me") # Use 'me' for the authenticated user context if supported, otherwise a specific UUID
if not CLIENT_ID or not CLIENT_SECRET:
raise ValueError("Environment variables CXONE_CLIENT_ID and CXONE_CLIENT_SECRET are required.")
client = CxoneClient(
base_url=BASE_URL,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
scopes=["platform-user-read"]
)
try:
user_data = client.get_user(USER_ID)
print(f"User Name: {user_data.get('name')}")
print(f"User Email: {user_data.get('emailAddress')}")
except Exception as e:
print(f"Error: {e}")
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Invalid Client ID or Client Secret.
- Fix: Verify the credentials in the NICE CXone Admin Console. Ensure there are no trailing spaces in the environment variables.
- Code Check: Print the
response.textto see the specific error message from the OAuth server.
Error: 403 Forbidden
- Cause: The access token is valid, but it lacks the required scope for the API endpoint being called.
- Fix: Check the documentation for the target API endpoint to see which scopes are required. Add those scopes to the
scopeparameter in the token request. For example, if calling/api/v2/users, you needplatform-user-read. - Code Check: Ensure the
scopestring inget_cxone_tokenincludes all necessary scopes separated by spaces.
Error: 429 Too Many Requests
- Cause: You have exceeded the rate limit for the OAuth endpoint or the subsequent API endpoint.
- Fix: Implement exponential backoff. Do not retry immediately. Wait for the
Retry-Afterheader value if present. - Code Check: Add logic to catch
HTTPErrorwith status code 429 and sleep for a calculated duration before retrying.
Error: invalid_grant
- Cause: The refresh token is invalid, expired, or has already been used.
- Fix: Refresh tokens are single-use. If you receive this error during a refresh, discard the old token and perform a new
client_credentialsauthentication to get a fresh pair of access and refresh tokens.