Client Credentials vs Auth Code for a nightly reporting script

We’re spinning up a nightly reporting job in Genesys Cloud v2 and the OAuth docs are just giving us the runaround on grant types. Sticking with client_credentials on the /oauth/token endpoint works fine, but the analytics:interaction:read scope keeps throwing a 403 on the metrics endpoints and we don’t see why. Here’s the curl we’re testing: curl -X POST https://api.mypurecloud.com/oauth/token -d "grant_type=client_credentials&client_id=OUR_ID&client_secret=OUR_SECRET&scope=analytics:interaction:read", which refreshes fine but the script just bails on the first call.

The analytics:interaction:read scope is restricted for service accounts. It’s a compliance thing. The platform blocks non-interactive tokens from pulling PII or detailed interaction logs to prevent data leakage. You’ll hit that 403 every time if you stick with client_credentials.

The fix is to switch to authorization_code with PKCE. You’ll need a human user to authorize the app once, then store the refresh token. Here’s how it usually goes.

First, register the app in the admin console. Make sure it’s set to “Confidential” and the callback URL is valid. Then hit the authorize endpoint with the correct scopes.

curl -X POST https://api.mypurecloud.com/oauth/token \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "grant_type=authorization_code&code=<AUTH_CODE>&redirect_uri=https://your-app.com/callback&client_id=<YOUR_CLIENT_ID>&client_secret=<YOUR_CLIENT_SECRET>"

The response gives you an access token and a refresh token. Save that refresh token securely. Use it to get new access tokens ly.

curl -X POST https://api.mypurecloud.com/oauth/token \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "grant_type=refresh_token&refresh_token=<YOUR_REFRESH_TOKEN>&client_id=<YOUR_CLIENT_ID>&client_secret=<YOUR_CLIENT_SECRET>"

This works because the token is tied to a user account that has the necessary permissions. Just make sure the user doesn’t leave the company or change roles, or your script breaks. It’s annoying to set up initially, but it’s the only way to get those analytics scopes working reliably. Don’t bother trying to force the service account. It’s not going to work.

PKCE is solid, but if this is purely backend, why not use a delegated token? Have a service user grant the app authority once. You can then swap tokens via the /oauth/token endpoint using the urn:ietf:params:oauth:grant-type:jwt-bearer grant. It keeps the PII access controlled without needing a human in the loop for refresh cycles.

// Don’t cache this. It expires in 30 minutes.
const token = await platformClient.OauthApi.postOauthToken({
grantType: ‘urn:ietf:params:oauth:grant-type:jwt-bearer’,
subjectToken: serviceUserToken,
scope: ‘analytics:interaction:read’
});


Be careful with JWT bearer grants. The access token is short-lived. You'll get **401 Unauthorized** if your script doesn't refresh it mid-run. I've seen ly jobs fail halfway through because the token died. Check the `expires_in` field and implement a retry loop. It's annoying but necessary.