CXone client_credentials grant returning 401 Bad Request despite valid credentials

We are integrating a .NET 6 custom agent desktop with NICE CXone. The requirement is to authenticate against the CXone API using the client_credentials grant type to fetch user profiles without interactive login. The service runs on US/Pacific servers.

I have the client ID and secret stored in our secret vault. The endpoint is https://api.nice-incontact.com/oauth/token. I am constructing the POST request manually using HttpClient because the CXone SDK does not expose a simple token helper for this specific flow in .NET yet.

Here is the code snippet:

var client = new HttpClient();
var content = new FormUrlEncodedContent(new[]
{
 new KeyValuePair<string, string>("grant_type", "client_credentials"),
 new KeyValuePair<string, string>("client_id", _clientId),
 new KeyValuePair<string, string>("client_secret", _clientSecret),
 new KeyValuePair<string, string>("scope", "user.profile:read")
});

var response = await client.PostAsync("https://api.nice-incontact.com/oauth/token", content);

The response status code is always 401 Unauthorized. The response body contains {"error":"invalid_client","error_description":"Client authentication failed"}.

I have verified the client ID and secret by copying them directly from the CXone admin console. I have also tried URL-encoding the credentials, but that did not help. The grant_type parameter is definitely client_credentials. The scope user.profile:read is valid according to the docs.

Is there a specific header requirement for CXone OAuth endpoints that is different from Genesys Cloud? I tried adding Content-Type: application/x-www-form-urlencoded explicitly, but the FormUrlEncodedContent sets that automatically.

The request works fine in Postman with the same credentials. This makes me think it is a subtle issue with how HttpClient is sending the payload or the encoding.

I checked the network trace. The request body looks correct. No extra characters. The host header is set correctly.

Can someone provide a working C# example for CXone client credentials flow? Or point out what I am missing in the request setup?