Client Credentials vs Authorization Code — which grant type for a server-side reporting app

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 requests library 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) and agent:login:login is NOT required.
  • SDK Version: genesyscloud Python 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

  1. Log in to the Genesys Cloud Admin Console.
  2. Navigate to Security > OAuth.
  3. Click Create Client.
  4. Set Client Type to Confidential.
  5. Grant the necessary scopes (e.g., analytics:conversation:view).
  6. 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 be client_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:

  1. Verify the client_id and client_secret match the OAuth Client in Genesys Cloud.
  2. Ensure the OAuth Client status is Active.
  3. 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:

  1. Go to Security > OAuth > Edit Client.
  2. Add the scope analytics:conversation:view.
  3. 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:

  1. Implement exponential backoff in your code.
  2. Reduce the frequency of requests.
  3. Use the Retry-After header 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:

  1. Ensure dateRange.from is before dateRange.to.
  2. Ensure dates are in ISO 8601 format with Z suffix for UTC.
  3. Validate the JSON structure against the API Specification.

Official References

3 Likes