We’re pulling queue state data directly from CXone for a custom analytics dashboard. The OAuth endpoint keeps rejecting the request even though the admin UI handles authentication without issue, and the background script can’t reuse that session. Payload structure appears correct, but the token endpoint drops a 401 every single time.
- Endpoint: POST https://api.nice.incontact.com/oauth2/token
- Payload: grant_type=client_credentials&client_id=abc123&client_secret=xyz789
- Response: {“error”:“invalid_client”,“error_description”:“Bad client credentials”}
- Tried standard URL encoding. Verified the client ID against the NICE portal. Stripped all whitespace from the secret string.
- Running requests from a US/Pacific EC2 instance using Python.
Still waiting on the exact header format that actually works.
You’re likely hitting the base64 encoding trap again. The CXone token endpoint is strict about the Authorization header format. If you’re using Python or similar, base64.b64encode returns bytes. Sending raw bytes or an improperly formatted string causes a 401 before the server even checks your credentials.
Here’s the exact curl command that works. Notice the --data-urlencode flags. They handle the form encoding properly. Also, ensure your app has admin:oauth scope. Without it, you get rejected instantly.
curl -X POST https://api.nice.incontact.com/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $(echo -n 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' | base64)" \
-d "grant_type=client_credentials" \
-d "scope=admin:oauth"
If you’re doing this in code, don’t construct the header manually if you can avoid it. Use a library that handles the Basic auth scheme. In Python, requests handles this nicely with the auth tuple.
import requests
url = "https://api.nice.incontact.com/oauth2/token"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
payload = {
"grant_type": "client_credentials",
"scope": "admin:oauth"
}
response = requests.post(url, auth=("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"), headers=headers, data=payload)
print(response.json())
Check the response. If it’s still 401, your client secret is wrong or the app is disabled. If it’s 400, check the scope. The admin UI uses a different flow, so don’t compare the two. They’re not the same.
Spot on. The Content-Type header needs to be application/x-www-form-urlencoded explicitly, otherwise CXone ignores the body entirely. Also make sure your client_secret isn’t URL-encoded twice if you’re using a helper library.