CXone API client_credentials grant failing with 400

Trying to set up a simple Python script to pull queue stats from CXone. I’m using the client_credentials grant type since this is a server-to-server integration, no user context needed. I have the client ID and secret from the developer console. The docs say to POST to /api/v2/oauth/token.

Here is the request body I’m sending:

import requests

url = "https://api.niceincontact.com/api/v2/oauth/token"
headers = {
 "Content-Type": "application/x-www-form-urlencoded"
}
payload = {
 "grant_type": "client_credentials",
 "client_id": "my_client_id_here",
 "client_secret": "my_secret_here"
}

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

I keep getting a 400 Bad Request. The error response is vague:

{
 "errors": [
 {
 "code": "invalid_request",
 "message": "Invalid grant type or missing parameters."
 }
 ]
}

I’ve double-checked the credentials. They work in the Swagger UI if I use the authorization_code flow with a user login. But for client_credentials, it always fails. Is there a specific scope I need to request? I tried adding "scope": "api" to the payload but same result. Also, do I need to base64 encode the client_id and secret in the header instead of sending them in the body? The examples online are messy. Some use Basic auth header, some put it in the form data. I’m using the form data approach like the Python requests example in the docs.

What am I missing? The endpoint seems right. The grant type is spelled correctly. Just not working.