Client Credentials vs Authorization Code — which grant type for a server-side reporting app
What You Will Build
- A Python script that authenticates to the Genesys Cloud CX API and retrieves conversation analytics data.
- This tutorial demonstrates the Client Credentials grant type, which is the correct choice for server-side, non-interactive reporting applications.
- The code uses the
requestslibrary and the official Genesys Cloud Python SDK to show both raw HTTP and SDK-based authentication flows.
Prerequisites
- OAuth Client Type: Confidential Client (Server-to-Server).
- Required Scopes:
analytics:conversation:view(for retrieving conversation details) andagent:login:loginis NOT required. - SDK Version:
genesyscloudPython SDK v10.0.0 or later. - Language/Runtime: Python 3.8+.
- External Dependencies:
pip install requests genesyscloud. - Genesys Cloud Environment: You must have an Oauth Client created in the Genesys Cloud Admin Console with the “Client Credentials” grant type enabled.
Authentication Setup
For a server-side reporting application, the Client Credentials grant is the industry standard. It allows your application to obtain an access token using only its client ID and client secret. This flow does not involve a user, does not require an authorization code, and does not store user-specific tokens.
The Authorization Code grant is designed for interactive user-facing applications (like a web dashboard where a specific agent logs in). Using it for a background reporting script introduces unnecessary complexity, requires managing user sessions, and violates the principle of least privilege if the script needs to access data across multiple users.
Generating the Client Credentials
- Log in to the Genesys Cloud Admin Console.
- Navigate to Security > OAuth.
- Click Create Client.
- Set Client Type to Confidential.
- Grant the necessary scopes (e.g.,
analytics:conversation:view). - Save and copy the Client ID and Client Secret.
Implementation
Step 1: Obtain Access Token via Raw HTTP
Before using the SDK, it is critical to understand the underlying HTTP mechanics. The Client Credentials flow involves a single POST request to the token endpoint.
Endpoint: https://api.mypurecloud.com/oauth/token
Request Body:
grant_type: Must beclient_credentials.client_id: Your OAuth Client ID.client_secret: Your OAuth Client Secret.
Here is the complete Python code to fetch the token using the requests library.
import requests
import json
from typing import Optional
def get_access_token(client_id: str, client_secret: str) -> Optional[str]:
"""
Obtains an OAuth2 access token using the Client Credentials grant.
Args:
client_id (str): The OAuth Client ID from Genesys Cloud.
client_secret (str): The OAuth Client Secret from Genesys Cloud.
Returns:
Optional[str]: The access token if successful, None otherwise.
"""
token_url = "https://api.mypurecloud.com/oauth/token"
# The request body must be form-encoded, not JSON
payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
try:
response = requests.post(token_url, data=payload, headers=headers)
response.raise_for_status() # Raises HTTPError for bad responses (4xx, 5xx)
token_data = response.json()
return token_data.get("access_token")
except requests.exceptions.HTTPError as http_err:
print(f"HTTP error occurred: {http_err}")
print(f"Response body: {response.text}")
except requests.exceptions.RequestException as req_err:
print(f"Request error occurred: {req_err}")
except ValueError:
print("Failed to parse JSON response")
return None
# Example Usage
CLIENT_ID = "your_client_id_here"
CLIENT_SECRET = "your_client_secret_here"
access_token = get_access_token(CLIENT_ID, CLIENT_SECRET)
if access_token:
print("Token acquired successfully.")
else:
print("Failed to acquire token.")
Step 2: Query Analytics Data Using the Token
Once you have the token, you use it in the Authorization: Bearer <token> header to call API endpoints. For reporting, we will query the Conversations Details endpoint.
Endpoint: POST https://api.mypurecloud.com/api/v2/analytics/conversations/details/query
This endpoint requires a JSON body defining the query parameters, such as the time range and the data elements to retrieve.
def fetch_conversation_details(access_token: str, start_time: str, end_time: str) -> dict:
"""
Queries Genesys Cloud for conversation details within a specific time range.
Args:
access_token (str): The OAuth2 access token.
start_time (str): ISO 8601 start time (e.g., "2023-10-01T00:00:00.000Z").
end_time (str): ISO 8601 end time (e.g., "2023-10-02T00:00:00.000Z").
Returns:
dict: The API response containing conversation details.
"""
api_url = "https://api.mypurecloud.com/api/v2/analytics/conversations/details/query"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
# Define the query body
query_body = {
"dateRange": {
"from": start_time,
"to": end_time
},
"groupBy": [],
"elements": [
"conversationId",
"direction",
"channel",
"startTime",
"endTime",
"duration",
"holdTime",
"wrapUpTime",
"queueName",
"queueId",
"agentName",
"agentId"
],
"filterBy": [],
"size": 100 # Max size per page
}
try:
response = requests.post(api_url, json=query_body, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as http_err:
if response.status_code == 401:
print("Authentication failed. Token may be expired or invalid.")
elif response.status_code == 403:
print("Forbidden. Check OAuth scopes. Required: analytics:conversation:view")
elif response.status_code == 429:
print("Rate limited. Implement exponential backoff.")
else:
print(f"HTTP error: {http_err}")
except requests.exceptions.RequestException as req_err:
print(f"Request error: {req_err}")
return {}
# Example Usage
START_TIME = "2023-10-01T00:00:00.000Z"
END_TIME = "2023-10-02T00:00:00.000Z"
if access_token:
data = fetch_conversation_details(access_token, START_TIME, END_TIME)
if data:
print(f"Retrieved {data.get('count', 0)} conversations.")
# Process data here
Step 3: Using the Genesys Cloud Python SDK
While raw HTTP is educational, production applications should use the official SDK. The SDK handles token refreshing, serialization, and pagination automatically.
Note: The Genesys Cloud Python SDK (genesyscloud) requires you to initialize the PlatformClient with the client ID and secret. It automatically manages the Client Credentials flow.
from genesyscloud.rest import Configuration
from genesyscloud.platform_client import PlatformClient
from genesyscloud.analytics_api import AnalyticsApi
from genesyscloud.model.conversation_detail_query import ConversationDetailQuery
from genesyscloud.model.conversation_element import ConversationElement
from datetime import datetime, timedelta
def fetch_data_with_sdk(client_id: str, client_secret: str):
"""
Fetches conversation details using the Genesys Cloud Python SDK.
"""
# 1. Initialize the Platform Client with Client Credentials
# The SDK automatically handles the OAuth token request
config = Configuration()
config.oauth_client_id = client_id
config.oauth_client_secret = client_secret
config.host = "https://api.mypurecloud.com"
# Initialize the client
platform_client = PlatformClient(config)
# 2. Create the Analytics API instance
analytics_api = AnalyticsApi(platform_client)
# 3. Define the Query
# Calculate time range (last 24 hours)
end_time = datetime.utcnow()
start_time = end_time - timedelta(days=1)
# Build the query object
query = ConversationDetailQuery(
date_range={
"from": start_time.isoformat() + "Z",
"to": end_time.isoformat() + "Z"
},
elements=[
"conversationId",
"channel",
"duration",
"agentName",
"queueName"
],
size=100
)
try:
# 4. Execute the Query
# The SDK handles pagination if you use the 'get' method with pagination helpers,
# but for a single request, we use 'post'
response = analytics_api.post_analytics_conversations_details_query(
body=query
)
print(f"Total conversations found: {response.count}")
if response.entities:
for conv in response.entities[:5]: # Print first 5
print(f"ID: {conv.conversation_id}, Agent: {conv.agent_name}")
except Exception as e:
print(f"SDK Error: {e}")
# Example Usage
# fetch_data_with_sdk(CLIENT_ID, CLIENT_SECRET)
Complete Working Example
This is a consolidated, production-ready Python script that combines token retrieval, error handling, and data fetching. It uses the raw requests library to demonstrate full control over the HTTP lifecycle, which is often preferred for simple reporting scripts to avoid heavy SDK dependencies.
#!/usr/bin/env python3
"""
Genesys Cloud CX Reporting Script
Authenticates via Client Credentials and retrieves conversation analytics.
"""
import os
import sys
import requests
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional
class GenesysReporter:
def __init__(self, client_id: str, client_secret: str):
self.client_id = client_id
self.client_secret = client_secret
self.token_url = "https://api.mypurecloud.com/oauth/token"
self.api_base = "https://api.mypurecloud.com"
self.access_token: Optional[str] = None
def authenticate(self) -> bool:
"""
Obtains an access token using Client Credentials grant.
Returns True if successful, False otherwise.
"""
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
try:
response = requests.post(self.token_url, data=payload, headers=headers, timeout=10)
response.raise_for_status()
self.access_token = response.json().get("access_token")
return True
except requests.exceptions.HTTPError as e:
print(f"Authentication failed: {e}")
print(f"Response: {response.text}")
return False
except Exception as e:
print(f"Unexpected error during authentication: {e}")
return False
def fetch_conversations(self, days_back: int = 1) -> List[Dict]:
"""
Fetches conversation details for the last N days.
"""
if not self.access_token:
print("Error: Not authenticated.")
return []
end_time = datetime.utcnow()
start_time = end_time - timedelta(days=days_back)
api_url = f"{self.api_base}/api/v2/anversations/details/query"
headers = {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
query_body = {
"dateRange": {
"from": start_time.isoformat() + "Z",
"to": end_time.isoformat() + "Z"
},
"elements": [
"conversationId",
"channel",
"duration",
"agentName",
"queueName",
"startTime"
],
"size": 1000, # Max allowed per page
"groupBy": []
}
all_conversations = []
try:
response = requests.post(api_url, json=query_body, headers=headers, timeout=30)
response.raise_for_status()
data = response.json()
all_conversations.extend(data.get("entities", []))
# Handle pagination if more data exists
while data.get("nextPage"):
# In a real scenario, you would fetch the next page using nextPage URL
# For this example, we stop after the first page for simplicity
print("Note: Pagination available but truncated for example.")
break
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
print("Token expired. Please re-authenticate.")
elif response.status_code == 403:
print("Permission denied. Check scopes.")
else:
print(f"API Error: {e}")
except Exception as e:
print(f"Request failed: {e}")
return all_conversations
def export_to_json(self, data: List[Dict], filename: str = "report.json"):
"""
Exports the retrieved data to a JSON file.
"""
with open(filename, 'w') as f:
json.dump(data, f, indent=2, default=str)
print(f"Report exported to {filename}")
def main():
# Load credentials from environment variables
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
if not client_id or not client_secret:
print("Error: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")
sys.exit(1)
reporter = GenesysReporter(client_id, client_secret)
if reporter.authenticate():
print("Authentication successful.")
conversations = reporter.fetch_conversations(days_back=1)
print(f"Retrieved {len(conversations)} conversations.")
if conversations:
reporter.export_to_json(conversations)
else:
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
Cause: The token is invalid, expired, or the client credentials are incorrect.
Fix:
- Verify the
client_idandclient_secretmatch the OAuth Client in Genesys Cloud. - Ensure the OAuth Client status is Active.
- Check if the token has expired. Client Credentials tokens typically last 1 hour. Re-run the authentication step.
Error: 403 Forbidden
Cause: The OAuth Client does not have the required scopes.
Fix:
- Go to Security > OAuth > Edit Client.
- Add the scope
analytics:conversation:view. - Save the client. Note: Scopes are checked at token issuance time. You must request a new token after adding scopes.
Error: 429 Too Many Requests
Cause: You have exceeded the rate limit for your organization or tenant.
Fix:
- Implement exponential backoff in your code.
- Reduce the frequency of requests.
- Use the
Retry-Afterheader in the response to determine how long to wait.
import time
def make_request_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.post(url, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
print(f"Rate limited. Waiting {retry_after} seconds...")
time.sleep(retry_after)
else:
return response
raise Exception("Max retries exceeded")
Error: 400 Bad Request
Cause: The query body is malformed or contains invalid date ranges.
Fix:
- Ensure
dateRange.fromis beforedateRange.to. - Ensure dates are in ISO 8601 format with
Zsuffix for UTC. - Validate the JSON structure against the API Specification.