OAuth2 Client Credentials returning 401 in Python requests despite valid client_id

Context:
I am attempting to generate an access token using the Genesys Cloud OAuth2 endpoint with Python requests. The client credentials were verified in the admin console.

import requests

url = "https://api.mypurecloud.com/oauth/token"
payload = {
 "grant_type": "client_credentials",
 "client_id": "valid_client_id_here",
 "client_secret": "valid_client_secret_here"
}
response = requests.post(url, data=payload)
print(response.status_code)
print(response.json())

Question:
Why does this setting result in a 401 Unauthorized error with {"error": "invalid_client"} when the credentials are correct and the grant type is explicitly set to client_credentials?

Yep, this is a known issue… the 401 usually means the client_id and client_secret must be passed as auth tuple, not in data. See KB-9921.

response = requests.post(url, data={"grant_type": "client_credentials"}, auth=("id", "secret"))

It depends, but generally… moving credentials to the auth tuple is correct for basic auth, but the risk here is how the data body is constructed. if you leave the payload empty or malformed, the token endpoint may still reject the request. in my react builds using implicit grants, i see similar strictness on payload structure.

the suggestion above about the auth tuple is valid, but ensure the grant_type is explicitly in the data dictionary. here is the robust pattern:

{
 "grant_type": "client_credentials",
 "scope": "admin:api"
}
  1. use auth=(client_id, client_secret) in the request header.
  2. pass the json object above as the data parameter.
  3. verify the scope matches your app permissions.

without the explicit scope, you might get a token with limited access, causing downstream 403 errors. this is a common gotcha in automation scripts.

It depends, but generally…

“OAuth2 Client Credentials returning 401 in Python requests despite valid client_id”

The documentation states, “The client credentials must be provided via HTTP Basic Authentication.” Ensure your requests call uses the auth parameter. Also verify the scope.

response = requests.post(url, data={"grant_type": "client_credentials", "scope": "admin:api"}, auth=(client_id, client_secret))

It depends, but typically the issue stems from how the requests library handles the authorization header when credentials are passed in the body versus the auth tuple.

In my Django analytics pipelines, I enforce strict authentication handling because a single 401 can cascade through Celery workers, causing unnecessary retries and database locks. The suggestion above is correct: Genesys Cloud’s OAuth2 endpoint requires HTTP Basic Authentication for client credentials, not form data. However, you must also ensure the scope parameter is included in the data payload, even if it’s a single value. Missing scopes often result in silent failures or 401s if the default scope isn’t configured.

Here is the robust pattern I use in my production scripts:

import requests

url = "https://api.mypurecloud.com/oauth/token"
# Credentials go here, NOT in the data payload
auth = ("valid_client_id_here", "valid_client_secret_here")

# Grant type and scope go in the data payload
payload = {
 "grant_type": "client_credentials",
 "scope": "admin:conversation:view admin:analytics:query"
}

headers = {
 "Content-Type": "application/x-www-form-urlencoded"
}

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

if response.status_code == 200:
 token_data = response.json()
 access_token = token_data['access_token']
 print("Token acquired successfully.")
else:
 print(f"Authentication failed: {response.status_code}")
 print(response.text)

Ensure your Content-Type is explicitly set to application/x-www-form-urlencoded. If you omit this, some proxy layers may misinterpret the body structure, leading to inconsistent 401 errors that are difficult to debug in distributed systems.