CXone Client Credentials Grant returning 400 invalid_grant

I’m trying to set up a simple Python script to authenticate against the NICE CXone API using the client credentials grant. The goal is to get a bearer token that I can use for subsequent API calls, but I keep hitting a wall.

Here is the code I’m running:

import requests

url = "https://platform.euw1.api.nicecxone.com/oauth/token"
payload = {
 "grant_type": "client_credentials",
 "client_id": "my-client-id",
 "client_secret": "my-client-secret"
}
headers = {
 "Content-Type": "application/x-www-form-urlencoded"
}

response = requests.post(url, data=payload, headers=headers)
print(response.status_code)
print(response.json())

The response I get back is:

400
{'error': 'invalid_grant', 'error_description': 'The authorization grant is invalid, expired, or revoked'}

I’ve double-checked the client ID and secret in the CXone admin console, and they look correct. I also tried adding the region header nice-access-region: euw1 but that didn’t change anything.

Here is what I have verified so far:

  • The client ID and secret are from a Service Account in the CXone portal.
  • The scope is set to api_platform.
  • I am using the correct endpoint for the euw1 region.
  • The credentials have not expired.

I’m not sure if I’m missing a header or if the payload format is wrong. The documentation isn’t super clear on this specific error. Any ideas on what might be causing this?

The docs state: “The client credentials grant requires the client_id and client_secret to be sent as form-encoded parameters.”

You’re sending a raw JSON body or a Python dict directly to requests.post. That hits the endpoint with Content-Type: application/json by default. The auth server expects application/x-www-form-urlencoded.

Here is the fix:

import requests

url = "https://platform.euw1.api.nicecxone.com/oauth/token"

# Use the data parameter, not json.
# This serializes the dict as form-encoded data.
payload = {
 "grant_type": "client_credentials",
 "client_id": "my-client-id",
 "client_secret": "my-client-secret"
}

response = requests.post(url, data=payload)

if response.status_code == 200:
 print(response.json())
else:
 print(f"Failed: {response.status_code} - {response.text}")

Also double check the base URL. platform.euw1.api.nicecxone.com looks right for the Europe West 1 region, but if your tenant is in usw2 or use1, you’ll get a 404 or DNS error before the auth logic even runs.

The 400 invalid_grant usually means the server parsed the request but couldn’t validate the credentials. If you switch to data=payload and it still fails, your secret is likely expired or copied with a trailing newline.