Looking for advice on a stubborn 401 Unauthorized error when fetching an access token via the Client Credentials flow using Python requests.
I have built a Node.js Express middleware that consumes GC webhooks, and I need a Python sidecar service to handle some heavy data processing. I am trying to get an auth token using the client_credentials grant type. My environment variables are set, and I have verified the client ID and secret in the GC admin console.
Here is the snippet:
import requests
import os
auth_url = "https://api.mypurecloud.com/oauth/token"
client_id = os.getenv('GC_CLIENT_ID')
client_secret = os.getenv('GC_CLIENT_SECRET')
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json"
}
payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "routing:queue:view"
}
response = requests.post(auth_url, data=payload, headers=headers)
print(response.status_code)
print(response.text)
The response is:
401
{“error”:“invalid_client”,“error_description”:“Client authentication failed”}
I am in the us-east-1 region. Is there a specific header requirement I am missing, or does the endpoint path vary by region? I have tried adding Authorization: Basic base64(client_id:client_secret) as well but get the same result.
this looks like a payload structure mismatch in the post request. the oauth2 token endpoint at /oauth/token is strict about content-type and body format. many python scripts fail here by sending application/json or using a dictionary that gets serialized incorrectly by the requests library.
the endpoint expects application/x-www-form-urlencoded. if you pass a dict to requests.post(..., data=payload), it works. if you pass json=payload, it sends json, which returns a 401 or 400.
here is the robust pattern i use in my terraform validation scripts:
import requests
AUTH_URL = "https://api.mypurecloud.com/oauth/token"
CLIENT_ID = os.environ["GENESYS_CLIENT_ID"]
CLIENT_SECRET = os.environ["GENESYS_CLIENT_SECRET"]
payload = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
# critical: do not use json=payload. use data=payload for form encoding
response = requests.post(AUTH_URL, data=payload)
if response.status_code != 200:
print(f"auth failed: {response.status_code}")
print(response.text)
else:
token = response.json().get("access_token")
check your console for the raw request body. if you see {"grant_type": ...} instead of grant_type=..., that is the error.
also verify the client secret has not been rotated. in my ci pipelines, i cache tokens for 50 minutes to avoid hitting the auth endpoint repeatedly, but for debugging, a fresh request is better. if the 401 persists, check the error field in the response json. it might say invalid_client if the credentials are wrong, or invalid_grant if the client is not configured for client_credentials flow in the admin console. ensure the client has the client_credentials grant type enabled under security > oauth clients.
Have you verified that you are using the data parameter instead of json in your requests.post call? The docs state: “The request body must be form-encoded,” so sending JSON will trigger a 401. Always inspect the raw request payload to ensure application/x-www-form-urlencoded is actually being sent.
The problem here is not just the content type, but often how the client_secret is handled in the Python requests library when using form-urlencoded data. While the previous suggestions correctly identify that application/x-www-form-urlencoded is mandatory, many developers still encounter 401s because they inadvertently send the secret as plain text in the body without proper encoding, or they miss the required scope parameter entirely.
In my experience building Genesys Cloud to Salesforce integrations, the OAuth token endpoint is extremely strict. If your token lacks the correct scopes, or if the client credentials are malformed during the form encoding process, the server returns a 401. Here is the robust pattern I use in my sidecar services:
- Define the payload explicitly as a dictionary.
- Include the scope. Even for client credentials, you must specify what you are accessing. For API calls, use
agent:read or similar.
- Use the
data parameter, never json.
import requests
# Your GC Org URL and Credentials
GENESYS_ORG_URL = "https://api.mypurecloud.com"
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
# The payload MUST be a dict for requests to encode it as x-www-form-urlencoded
payload = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "agent:read conversation:read" # Always specify scope
}
# Send the request
response = requests.post(
f"{GENESYS_ORG_URL}/oauth/token",
data=payload # This automatically sets Content-Type to application/x-www-form-urlencoded
)
if response.status_code == 200:
token = response.json()["access_token"]
print("Token acquired successfully.")
else:
print(f"Failed: {response.status_code} - {response.text}")
Refer to this internal guide on OAuth client credentials for more details on scope requirements.