Python requests OAuth2 client_credentials flow returning 400 for Genesys Cloud

Quick check on the correct payload structure for the client_credentials grant type. I’m switching a legacy Node script to Python requests to handle token renewal, but hitting a wall with the authorization endpoint.

Here’s the setup. Using genesyscloud-python v8.10.0 locally, but bypassing the SDK auth helper to understand the raw HTTP mechanics first. The goal is simple: get a bearer token for a service account.

The endpoint is https://api.mypurecloud.com/oauth/token.

Code snippet looks like this:

import requests
import json

url = "https://api.mypurecloud.com/oauth/token"
headers = {
 "Content-Type": "application/x-www-form-urlencoded",
 "Accept": "application/json"
}

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

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

The response is a 400 Bad Request. The JSON body says:

{
 "errors": [
 {
 "parameter": "client_id",
 "description": "Client identifier is invalid."
 }
 ]
}

This is strange. I copied the client_id and client_secret directly from the Genesys Cloud Admin console under Integrations > OAuth. The client is set to Confidential. The allowed grant types include client_credentials. I even tried encoding the credentials in the Authorization header as Basic Auth per some old forum posts, but that returns a 401.

Am I missing something obvious with the Content-Type? Maybe requests is mangling the form data? Or is there a specific scope required in the initial request for this grant type? The docs are a bit vague on the exact error messages for malformed client IDs versus actual permission issues.

Tried switching to application/json for the header but that just broke it faster. Stuck on the form-urlencoded part. Any ideas why the platform thinks the ID is invalid when it’s clearly right?