CXone client_credentials grant returns 401 from Kotlin backend

Trying to set up a service account flow to hit the CXone REST API from our Kotlin backend. We’re using the client_credentials grant type to get an access token, but the POST to https://platform.niceincontact.com/oauth2/token keeps returning a 401 Unauthorized.

The request looks standard. We’re sending application/x-www-form-urlencoded with grant_type, client_id, and client_secret. Here’s the Retrofit builder config:

val formBody = FormBody.Builder()
 .add("grant_type", "client_credentials")
 .add("client_id", config.cxoneClientId)
 .add("client_secret", config.cxoneClientSecret)
 .build()

val request = Request.Builder()
 .url("https://platform.niceincontact.com/oauth2/token")
 .post(formBody)
 .build()

The response body is just {"error":"invalid_client","error_description":"Client authentication failed"}. I’ve triple-checked the credentials in the CXone Admin portal under Integrations. The client ID matches exactly. The secret has no trailing spaces. We’re not using any special scopes yet, just trying to get the token first.

Is there a specific header missing? I tried adding Authorization: Basic base64(client_id:client_secret) but that made it worse. The docs are a bit sparse on the exact header requirements for this grant type when calling directly without an SDK wrapper. Any pointers?