Querying Conversation Analytics with Pagination in Genesys Cloud
What You Will Build
- A Python script that retrieves detailed conversation analytics data from Genesys Cloud using the
PureCloudPlatformClientV2SDK. - The script demonstrates how to handle pagination for the
/api/v2/analytics/conversations/details/queryendpoint, which uses cursor-based pagination rather than traditional page numbers. - The implementation covers authentication, request construction, iterative fetching, and robust error handling for rate limits and timeouts.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Grant) or Public Client (PKCE). For server-side scripts, a Confidential Client is standard.
- Required Scopes:
analytics:conversation:viewis mandatory. If you need to filter by specific user attributes or custom segments, additional scopes likeuser:readmay be required depending on the filter complexity. - SDK Version: Genesys Cloud Python SDK version 3.0.0 or higher.
- Language/Runtime: Python 3.8+.
- External Dependencies:
pip install purecloudplatformclientv2
Authentication Setup
Genesys Cloud APIs require OAuth 2.0 authentication. The Python SDK handles the token exchange and refresh automatically once initialized. You must provide your client ID, client secret, and environment (e.g., mypurecloud.com, euw2.pure.cloud, or au02.pure.cloud).
import os
from purecloudplatformclientv2 import ApiClient, Configuration, OAuth2Client
def get_auth_client() -> ApiClient:
"""
Initializes the API client with OAuth2 credentials.
Uses environment variables for security.
"""
client_id = os.environ.get("GENESYS_CLIENT_ID")
client_secret = os.environ.get("GENESYS_CLIENT_SECRET")
environment = os.environ.get("GENESYS_ENVIRONMENT", "mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set.")
# Configure the API client
config = Configuration()
config.host = f"https://{environment}"
# Initialize the OAuth2 client
oauth_client = OAuth2Client(client_id, client_secret)
# Create the API client instance
api_client = ApiClient(configuration=config, oauth_client=oauth_client)
return api_client
Implementation
Step 1: Constructing the Analytics Query Body
The /api/v2/analytics/conversations/details/query endpoint accepts a complex JSON body to define the query. Unlike simple GET endpoints, this is a POST request that allows for sophisticated filtering, grouping, and date range specification.
Key parameters in the body:
dateFrom/dateTo: ISO 8601 timestamps defining the analysis window.interval: The time bucket for aggregation (e.g.,PT1Hfor hourly).view: The analytics view to use (e.g.,default).groupBy: Dimensions to group results by (e.g.,mediaType,queueId).filter: Criteria to include/exclude specific conversations.
from purecloudplatformclientv2 import AnalyticsQueryDefinition
def build_query_body(start_date: str, end_date: str) -> dict:
"""
Constructs the request body for the conversation details query.
Args:
start_date: ISO 8601 string (e.g., '2023-10-01T00:00:00.000Z')
end_date: ISO 8601 string (e.g., '2023-10-08T00:00:00.000Z')
Returns:
Dictionary representing the AnalyticsQueryDefinition
"""
query_body = {
"dateFrom": start_date,
"dateTo": end_date,
"interval": "PT1H", # 1-hour intervals
"view": "default",
"groupBy": ["mediaType"],
"filter": {
"and": [
{
"dimension": "queueId",
"operator": "eq",
"value": "your-queue-id-here" # Replace with actual queue ID
}
]
},
"select": [
"wrapUpCode",
"mediaType",
"totalHandleTime",
"talkTime",
"holdTime",
"holdCount"
]
}
return query_body
Step 2: Handling Cursor-Based Pagination
The Genesys Cloud Analytics API uses cursor-based pagination. This is distinct from offset-based pagination (page 1, page 2).
Why Cursor-Based?
- Consistency: New data inserted during the query does not cause duplicate or missing rows.
- Performance: The server uses the cursor as an index pointer, avoiding expensive
OFFSETcalculations on large datasets. - Statelessness: The client only needs to pass the
cursorstring returned from the previous response to get the next batch.
The Loop Logic:
- Send the initial query without a
cursor. - Process the
entities(conversations) in the response. - Check the
nextPageUrior thecursorfield in the response metadata. - If a cursor exists, append it to the request body and repeat.
- Stop when the response indicates no more pages (usually
nextPageUriis null or empty).
from purecloudplatformclientv2 import AnalyticsApi
from purecloudplatformclientv2.rest import ApiException
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def fetch_conversation_details(api_client: ApiClient, query_body: dict, max_pages: int = 100):
"""
Iterates through paginated results using cursor-based pagination.
Args:
api_client: Initialized PureCloudPlatformClientV2 ApiClient
query_body: The AnalyticsQueryDefinition dictionary
max_pages: Safety limit to prevent infinite loops
Returns:
List of conversation detail objects
"""
analytics_api = AnalyticsApi(api_client)
all_conversations = []
page_count = 0
cursor = None
while page_count < max_pages:
page_count += 1
logger.info(f"Fetching page {page_count}, cursor: {cursor}")
try:
# The SDK method for this endpoint is post_analytics_conversations_details_query
# We pass the query body. The SDK handles the POST request.
# Note: The SDK does not automatically paginate. We must manage the cursor.
# If we have a cursor from the previous page, we must add it to the body
if cursor:
query_body["cursor"] = cursor
response = analytics_api.post_analytics_conversations_details_query(
body=query_body
)
# Extract the entities (conversations)
entities = response.entities if response.entities else []
all_conversations.extend(entities)
logger.info(f"Retrieved {len(entities)} conversations on page {page_count}")
# Check for next page cursor
# In the Genesys Cloud Analytics response, the cursor is typically in the 'nextPageUri'
# or explicitly in a 'cursor' field if the SDK model exposes it.
# For this specific endpoint, the response object usually contains 'nextPageUri'.
# However, the raw JSON response often has a 'cursor' field.
# The Python SDK model 'AnalyticsConversationDetailsQueryResponse' has a 'next_page_uri' attribute.
next_page_uri = response.next_page_uri
if next_page_uri:
# Extract cursor from the URI if necessary, or use the SDK's helper if available.
# The Genesys Cloud API usually returns the full URI for the next page.
# We need to parse the cursor parameter from the URI.
# Example URI: .../query?cursor=abc123xyz
if "cursor=" in next_page_uri:
cursor = next_page_uri.split("cursor=")[1].split("&")[0]
else:
# Fallback: if the API returns a direct cursor field in the body (less common in SDK models)
# Check if the raw response has it. For robustness, we rely on nextPageUri parsing.
logger.warning("Next page URI found but no cursor parameter detected. Stopping.")
break
else:
# No more pages
logger.info("No more pages found. Pagination complete.")
break
# Optional: Small delay to respect rate limits and avoid 429s
time.sleep(1)
except ApiException as e:
logger.error(f"API Exception: {e}")
if e.status == 429:
logger.warning("Rate limited. Waiting 10 seconds before retrying.")
time.sleep(10)
page_count -= 1 # Retry the same page
continue
elif e.status == 401 or e.status == 403:
logger.error("Authentication or Authorization failed. Check scopes and token.")
break
else:
raise e
return all_conversations
Step 3: Processing and Exporting Results
Once the data is fetched, it is typically useful to export it to CSV or JSON for further analysis. The entities list contains detailed objects for each conversation.
import csv
import json
from datetime import datetime
def export_to_csv(conversations: list, filename: str = "conversation_details.csv"):
"""
Exports the list of conversation detail objects to a CSV file.
"""
if not conversations:
logger.warning("No conversations to export.")
return
# Define headers based on common fields
headers = [
"conversationId", "mediaType", "wrapUpCode", "totalHandleTime",
"talkTime", "holdTime", "holdCount", "startTime", "endTime"
]
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=headers, extrasaction='ignore')
writer.writeheader()
for conv in conversations:
# Flatten the object for CSV
row = {
"conversationId": conv.conversation_id,
"mediaType": conv.media_type,
"wrapUpCode": conv.wrap_up_code if hasattr(conv, 'wrap_up_code') else "",
"totalHandleTime": conv.total_handle_time if hasattr(conv, 'total_handle_time') else "",
"talkTime": conv.talk_time if hasattr(conv, 'talk_time') else "",
"holdTime": conv.hold_time if hasattr(conv, 'hold_time') else "",
"holdCount": conv.hold_count if hasattr(conv, 'hold_count') else "",
"startTime": conv.start_time if hasattr(conv, 'start_time') else "",
"endTime": conv.end_time if hasattr(conv, 'end_time') else ""
}
writer.writerow(row)
logger.info(f"Exported {len(conversations)} conversations to {filename}")
Complete Working Example
This script combines authentication, query construction, pagination logic, and export functionality into a single runnable module.
import os
import time
import logging
import csv
from purecloudplatformclientv2 import ApiClient, Configuration, OAuth2Client, AnalyticsApi
from purecloudplatformclientv2.rest import ApiException
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def get_auth_client() -> ApiClient:
"""Initializes the API client with OAuth2 credentials."""
client_id = os.environ.get("GENESYS_CLIENT_ID")
client_secret = os.environ.get("GENESYS_CLIENT_SECRET")
environment = os.environ.get("GENESYS_ENVIRONMENT", "mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set.")
config = Configuration()
config.host = f"https://{environment}"
oauth_client = OAuth2Client(client_id, client_secret)
api_client = ApiClient(configuration=config, oauth_client=oauth_client)
return api_client
def build_query_body(start_date: str, end_date: str, queue_id: str) -> dict:
"""Constructs the AnalyticsQueryDefinition body."""
return {
"dateFrom": start_date,
"dateTo": end_date,
"interval": "PT1H",
"view": "default",
"groupBy": ["mediaType"],
"filter": {
"and": [
{
"dimension": "queueId",
"operator": "eq",
"value": queue_id
}
]
},
"select": [
"conversationId",
"mediaType",
"wrapUpCode",
"totalHandleTime",
"talkTime",
"holdTime",
"holdCount",
"startTime",
"endTime"
]
}
def fetch_all_conversations(api_client: ApiClient, query_body: dict, max_pages: int = 50) -> list:
"""
Fetches all conversations using cursor-based pagination.
"""
analytics_api = AnalyticsApi(api_client)
all_conversations = []
page_count = 0
cursor = None
while page_count < max_pages:
page_count += 1
logger.info(f"Page {page_count}: Fetching with cursor '{cursor}'")
try:
# Add cursor to body if it exists
if cursor:
query_body["cursor"] = cursor
# Execute the query
response = analytics_api.post_analytics_conversations_details_query(
body=query_body
)
# Collect entities
entities = response.entities if response.entities else []
all_conversations.extend(entities)
logger.info(f"Page {page_count}: Retrieved {len(entities)} records.")
# Determine next cursor
next_page_uri = response.next_page_uri
if next_page_uri and "cursor=" in next_page_uri:
cursor = next_page_uri.split("cursor=")[1].split("&")[0]
else:
logger.info("End of data reached.")
break
# Respect rate limits
time.sleep(1)
except ApiException as e:
logger.error(f"API Error on page {page_count}: Status {e.status}, Message {e.body}")
if e.status == 429:
logger.warning("Rate limit hit. Waiting 10s...")
time.sleep(10)
page_count -= 1 # Retry same page
continue
elif e.status in [401, 403]:
logger.error("Auth failed. Exiting.")
break
else:
raise e
return all_conversations
def export_conversations(conversations: list, filename: str = "gen_conversations.csv"):
"""Exports conversations to CSV."""
if not conversations:
logger.warning("No data to export.")
return
headers = [
"conversationId", "mediaType", "wrapUpCode", "totalHandleTime",
"talkTime", "holdTime", "holdCount", "startTime", "endTime"
]
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=headers, extrasaction='ignore')
writer.writeheader()
for conv in conversations:
row = {
"conversationId": getattr(conv, 'conversation_id', None),
"mediaType": getattr(conv, 'media_type', None),
"wrapUpCode": getattr(conv, 'wrap_up_code', None),
"totalHandleTime": getattr(conv, 'total_handle_time', None),
"talkTime": getattr(conv, 'talk_time', None),
"holdTime": getattr(conv, 'hold_time', None),
"holdCount": getattr(conv, 'hold_count', None),
"startTime": getattr(conv, 'start_time', None),
"endTime": getattr(conv, 'end_time', None)
}
writer.writerow(row)
logger.info(f"Successfully exported {len(conversations)} rows to {filename}")
def main():
# Configuration
QUEUE_ID = os.environ.get("GENESYS_QUEUE_ID", "default-queue-id")
START_DATE = "2023-10-01T00:00:00.000Z"
END_DATE = "2023-10-08T00:00:00.000Z"
try:
# 1. Authenticate
logger.info("Initializing API Client...")
api_client = get_auth_client()
# 2. Build Query
logger.info("Building query body...")
query_body = build_query_body(START_DATE, END_DATE, QUEUE_ID)
# 3. Fetch Data
logger.info("Starting pagination fetch...")
conversations = fetch_all_conversations(api_client, query_body)
# 4. Export
logger.info("Exporting data...")
export_conversations(conversations)
logger.info("Process completed successfully.")
except Exception as e:
logger.error(f"Fatal error: {e}")
raise
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token has expired, or the client ID/secret is incorrect.
- Fix: Ensure
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRETare correct. The SDK handles token refresh automatically, but if the initial grant fails, check credentials. Verify the client has theanalytics:conversation:viewscope assigned in the Admin Console under Organization > Security > OAuth 2.0.
Error: 403 Forbidden
- Cause: The OAuth client lacks the necessary permissions.
- Fix: Confirm that the OAuth client is assigned the
analytics:conversation:viewscope. Also, ensure the user associated with the service account (if using user-based auth) has access to the analytics views.
Error: 429 Too Many Requests
- Cause: You have exceeded the API rate limit for the analytics endpoint.
- Fix: Implement exponential backoff. The example above includes a
time.sleep(1)between pages. For high-volume queries, consider reducing the date range (dateFromtodateTo) to fewer days per request and running the script multiple times.
Error: 500 Internal Server Error
- Cause: The query body is malformed or the date range is too large for the engine to process in one go.
- Fix: Validate the ISO 8601 format of
dateFromanddateTo. Ensureintervalis valid (e.g.,PT1H,PT1D). If the date range is larger than 6 months, split it into smaller chunks.