401 Unauthorized on /api/v2/oauth/token with Python requests library

I can’t seem to figure out why my Python script returns a 401 Unauthorized error when attempting to obtain an OAuth2 access token using the Client Credentials grant type. I am using the standard requests library, not the official Genesys SDK, to keep dependencies minimal for this data action trigger.

The endpoint is https://{my_organization}.mygenesyscloud.com/api/v2/oauth/token. I have verified the client_id and client_secret are correct and have the necessary scope admin:application. The error occurs immediately upon posting.

Here is the payload structure I am sending in the request body:

{
 "grant_type": "client_credentials",
 "client_id": "my-client-id",
 "client_secret": "my-client-secret"
}

The response body is simply {"error":"unauthorized"}. I have tried setting the Content-Type header to application/x-www-form-urlencoded explicitly, as well as relying on the default behavior of the requests.post(data=...) method. I am in Asia/Seoul timezone, but that should not affect token generation. What am I missing in the request configuration?

Make sure you are URL-encoding the client credentials or sending them as form data, not in the header.

import requests

resp = requests.post(
 f"https://{org}.mygenesyscloud.com/api/v2/oauth/token",
 data={"grant_type": "client_credentials"},
 auth=(client_id, client_secret)
)

The auth tuple handles the Basic Auth header automatically. If you send raw strings in the header without encoding, the token endpoint rejects it with 401.

Have you tried inspecting the raw request headers? The suggestion above is correct, but it misses a critical detail about how requests handles the Content-Type header when using the data parameter.

  1. Ensure you are passing a dictionary to data, not a JSON string.
  2. Explicitly set headers={"Content-Type": "application/x-www-form-urlencoded"}.

If you omit the header, requests might default to something else or add charset=utf-8, which the Genesys OAuth endpoint rejects with a 401.

import requests

url = f"https://{org}.mygenesyscloud.com/api/v2/oauth/token"
payload = {"grant_type": "client_credentials"}
headers = {"Content-Type": "application/x-www-form-urlencoded"}

response = requests.post(
 url, 
 data=payload, 
 auth=(client_id, client_secret),
 headers=headers
)

print(response.status_code)
print(response.json())

Warning: Do not use json=payload for this endpoint. The OAuth spec requires form-encoded data, not JSON. Sending JSON here will always fail authentication.

Have you tried wrapping the grant type in a dictionary passed to the data argument to ensure proper URL encoding?

Parameter Value
grant_type client_credentials

The 401 usually stems from malformed form data rather than invalid credentials.

If I remember correctly… the auth tuple in requests applies Basic Auth to the /api/v2/oauth/token endpoint, which is correct. However, using raw client credentials here introduces a critical security risk for downstream speech analytics pipelines.

You should never embed secrets directly in the script. Rotate them immediately and use environment variables or a vault. The 401 might actually stem from an expired secret or incorrect scope configuration on the OAuth application, not just the header format. Check the scope parameter in the POST body to ensure it includes analytics:report:read or similar, depending on your use case.

import os
import requests

token_url = f"https://{os.getenv('GENESYS_ORG')}.mygenesyscloud.com/api/v2/oauth/token"
payload = {
 "grant_type": "client_credentials",
 "scope": "analytics:report:read analytics:speech:read"
}
auth = (os.getenv('CLIENT_ID'), os.getenv('CLIENT_SECRET'))

response = requests.post(token_url, data=payload, auth=auth)
print(response.status_code, response.json())