Does anyone know why my POST to /oauth/token returns a 401 Unauthorized when I try to generate a long-lived API token for a CI/CD pipeline? The documentation says “Provide client credentials to obtain an access token.” I am sending this JSON payload: {“grant_type”: “client_credentials”, “client_id”: “myapp”, “client_secret”: “secret123”, “duration”: “long”}. The flow fails immediately. I need the token to run automated Data Action calls without manual login. Why is the duration parameter ignored?
TL;DR: Remove the duration field and ensure Content-Type is application/x-www-form-urlencoded.
This looks like a classic client credential flow mismatch. The Genesys Cloud OAuth endpoint does not accept JSON payloads by default for the standard token grant, nor does it recognize a duration parameter in the request body. You are likely hitting a 401 because the server cannot parse the credentials or rejects the unknown field.
Here is the corrected curl command using form-encoded data:
curl -X POST "https://api.mypurecloud.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=your_client_id&client_secret=your_client_secret"
If you are using Python with requests, use the data parameter instead of json:
import requests
resp = requests.post(
"https://api.mypurecloud.com/oauth/token",
data={
"grant_type": "client_credentials",
"client_id": env.CLIENT_ID,
"client_secret": env.CLIENT_SECRET
}
)
The token returned is valid for 900 seconds. If you need a “long-lived” token for CI/CD, you should generate an API Token via the Admin UI or the /api/v2/oauth/apitokens endpoint, which allows you to set expiration dates explicitly. The client credentials flow is designed for short-lived service-to-service auth.
TL;DR: The 401 is a symptom of malformed content, but the real risk is using client credentials for CI/CD long-lived tokens without strict scope isolation.
Make sure you are sending the payload as application/x-www-form-urlencoded, not JSON. The Genesys Cloud OAuth 2.0 implementation for the client_credentials grant type strictly expects form-encoded data. Sending JSON triggers a 400 Bad Request or 401 Unauthorized because the server cannot extract the client_id and client_secret from the body. The duration parameter is also invalid in this context and causes immediate rejection.
Here is the correct curl command for testing:
curl -X POST "https://api.mypurecloud.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET"
In your CI/CD pipeline, use the SDK’s OAuthClient to handle this securely. Do not hardcode secrets.
OAuthClient oAuthClient = platformClient.getOAuthClient();
OAuthClientCredentialsCredentials credentials = new OAuthClientCredentialsCredentials();
credentials.setClientId("YOUR_CLIENT_ID");
credentials.setClientSecret("YOUR_CLIENT_SECRET");
// Use a specific scope, not 'default'
List<String> scopes = Arrays.asList("analytics:reports:read");
credentials.setScopes(scopes);
OAuthClientCredentialsTokenResponse tokenResponse = oAuthClient.postOAuthToken(credentials);
String accessToken = tokenResponse.getAccessToken();
Warning: Client credentials tokens do not expire quickly by default unless configured in the app settings. If your CI/CD job fails, the token remains valid until revoked. This creates a security gap where automated scripts might run with stale permissions. Always rotate secrets and restrict scopes to the minimum required for the Data Action calls. I see this mistake often in mobile backend integrations where devs copy-paste token logic without checking the endpoint requirements.
This is a standard form-encoding mismatch. The endpoint rejects JSON payloads for client credentials.
curl -X POST https://api.mypurecloud.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=ID&client_secret=SECRET"
Switch to URL-encoded data. The duration parameter is ignored anyway.