Python requests OAuth2 Client Credentials returns 400

Can’t get this config to load properly when trying to fetch a Genesys Cloud access token using requests and client credentials. The endpoint /oauth/token returns a 400 Bad Request with invalid_grant, even though the credentials are verified.

The grant_type parameter must be set to client_credentials for machine-to-machine authentication.

Here is the payload I am sending:

import requests

url = 'https://api.mypurecloud.com/oauth/token'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
payload = {
 'grant_type': 'client_credentials',
 'client_id': 'MY_CLIENT_ID',
 'client_secret': 'MY_CLIENT_SECRET'
}

response = requests.post(url, headers=headers, data=payload)
print(response.status_code)
print(response.json())

I am in Europe/Stockholm, so clock skew is not an issue. What am I missing in the request body?

Depends on your setup, but generally the invalid_grant error in Genesys Cloud OAuth2 flows usually stems from a mismatch in the client secret encoding or the specific scope requirements for analytics APIs. When using Python requests, the default behavior for POST data is application/x-www-form-urlencoded, which is correct, but you must ensure the client_secret is not URL-encoded if it contains special characters like + or /. The API expects raw form parameters.

Here is the robust pattern I use for my dashboard fetchers. Notice the explicit data dictionary and the header configuration. We also need to ensure the scope matches our analytics needs, such as analytics:metrics:view.

import requests

def get_gc_token():
 url = "https://api.mypurecloud.com/oauth/token"
 
 # Ensure these match your Integration settings exactly
 payload = {
 "grant_type": "client_credentials",
 "client_id": "YOUR_CLIENT_ID",
 "client_secret": "YOUR_CLIENT_SECRET", 
 "scope": "analytics:metrics:view analytics:report:view"
 }
 
 headers = {
 "Content-Type": "application/x-www-form-urlencoded",
 "Accept": "application/json"
 }
 
 response = requests.post(url, data=payload, headers=headers)
 
 if response.status_code == 400:
 print(f"Error details: {response.json()}")
 # Check for 'invalid_grant' vs 'invalid_client'
 elif response.status_code == 200:
 return response.json().get("access_token")
 else:
 raise Exception(f"OAuth Failed: {response.status_code}")

token = get_gc_token()

The key is the Content-Type header. If you omit this, requests might infer it incorrectly based on the payload structure, causing the server to reject the grant type parsing. Also, verify your Integration in Genesys Cloud is enabled and the client secret has not been rotated recently.

Warning: Do not use params for the payload; always use data. Using params sends the credentials in the URL query string, which is rejected by the OAuth server for security reasons and will result in a 400 error regardless of credential validity.

The quickest way to solve this is… Verify your scope permissions. The invalid_grant error often masks a lack of admin:platform or specific analytics scopes. Ensure your client ID has the correct OAuth scopes attached in the Developer Portal. Here is the corrected Python implementation using requests with proper form encoding:

import requests

payload = {
 'grant_type': 'client_credentials',
 'scope': 'admin:platform analytics:reporting:read'
}
auth = ('YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET')
response = requests.post(
 'https://api.mypurecloud.com/oauth/token',
 auth=auth,
 data=payload
)
print(response.json())
  1. Use auth tuple for Basic Auth header generation.
  2. Pass scopes as a space-separated string in the payload.
  3. Check response status. If it fails, log the raw response body to identify missing scopes. This matches the standard Apollo gateway authentication pattern I use for batching.

I’d recommend looking at at the WebSocket lifecycle instead of fighting OAuth timeouts. The Notification API handles token refresh automatically via 401 close codes, so you only need a stable initial handshake.

Component Requirement
Endpoint wss://api.mypurecloud.com/api/v2/notifications
Header Authorization: Bearer <token>
Payload JSON subscription request
import websockets
import json

uri = "wss://api.mypurecloud.com/api/v2/notifications"
headers = {"Authorization": f"Bearer {access_token}"}

async with websockets.connect(uri, extra_headers=headers) as ws:
 await ws.send(json.dumps({
 "id": 1,
 "method": "subscribe",
 "params": {"channels": ["routing/queueMetrics"]}
 }))
 async for msg in ws:
 print(msg)

It depends, but generally…

Python requests defaults to application/x-www-form-urlencoded, which is correct. However, invalid_grant often indicates a scope mismatch or client secret encoding issue. In Angular services, we strictly validate scopes like admin:platform before calling /oauth/token. Ensure your Python payload explicitly includes grant_type=client_credentials and matches the registered scopes exactly.