CXone Client Credentials flow returns 400 invalid_grant despite correct secret

I’m trying to implement a server-side authentication flow for our backend services using the CXone API. The goal is to use the client_credentials grant type so we don’t have to manage user sessions. I have the client_id and client_secret from the Admin console, and I’ve verified they match exactly.

The endpoint I’m hitting is https://api.mypurecloud.com/oauth/token. Here is the Kotlin code I’m using with khttp:

val body = "grant_type=client_credentials&client_id=$clientId&client_secret=$clientSecret"
val response = post("https://api.mypurecloud.com/oauth/token") {
 addHeader("Content-Type", "application/x-www-form-urlencoded")
 setBody(body)
}

The response comes back with a 400 Bad Request. The JSON payload is:

{
 "error": "invalid_grant",
 "error_description": "The provided authorization grant is invalid, expired, or revoked"
}

This is confusing because the credentials are fresh. I tried adding scope=openid to the body, but that didn’t change anything. The docs say client_credentials doesn’t need a scope parameter, but I’ve seen some examples where it’s included.

Is there a specific scope required for this grant type in CXone? Or is the URL encoding messing up the client_secret if it contains special characters? I’ve tried URL encoding the whole body string, but that seems to break the parsing on their end.

Also, I noticed in our Genesys Cloud setup, the OAuth client has to be explicitly configured for confidential clients. I assume CXone is similar, but the console doesn’t show a clear toggle for “allow client credentials”.

We’re using the standard REST API, not the SDK, because we need to call some endpoints that the SDK doesn’t expose yet.

Any ideas on what’s triggering the invalid_grant? I’ve checked the logs and there’s no rate limiting happening. Just a straight 400.

I can provide the raw curl command if that helps. I’m using Kotlin 1.9.22 and khttp 0.3.0. The timezone is America/Chicago, but I doubt that matters for token generation.

Let me know if I’m missing a header or a body parameter. The documentation is a bit sparse on the exact body format for this grant type compared to the auth code flow.

Are you sending the client secret in the body or via Basic Auth header? The docs say form-urlencoded, but some libraries handle it differently.

In CXone I usually hit the REST Proxy action. It handles the OAuth handshake automatically if you configure the connection correctly. For raw API calls, make sure grant_type is exactly client_credentials.

Here is a curl example that works for me:

curl -X POST "https://api.mypurecloud.com/oauth/token" \
 -H "Authorization: Basic $(echo -n 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' | base64)" \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "grant_type=client_credentials"

The 400 error often means the base64 encoding of the credentials is wrong or the secret has a trailing space. Check for invisible characters in the secret. Copy it fresh from the Admin console. If it still fails, try the REST Proxy in . It’s less friction.