Paginating Genesys Cloud Analytics Conversation Details with Cursors
What You Will Build
- You will build a robust pagination handler that retrieves historical conversation details from Genesys Cloud Analytics.
- You will use the
/api/v2/analytics/conversations/details/queryendpoint to fetch data in manageable chunks. - You will implement this in Python using the official Genesys Cloud SDK (
genesyscloud).
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Grant).
- Required Scopes:
analytics:conversation:details:view,analytics:conversation:summary:view. - SDK Version:
genesyscloud>= 130.0.0 (Python). - Runtime: Python 3.8+.
- Dependencies:
pip install genesyscloud.
Authentication Setup
Genesys Cloud uses OAuth 2.0 for authentication. For server-to-server integrations, the Client Credentials flow is the standard. The Genesys Cloud Python SDK handles token acquisition and refresh automatically if configured correctly.
You must instantiate the PureCloudPlatformClientV2 with your environment, client ID, and client secret.
from genesyscloud import PureCloudPlatformClientV2
def get_platform_client(
env: str = "us-east-1",
client_id: str = None,
client_secret: str = None
) -> PureCloudPlatformClientV2:
"""
Initialize and return an authenticated Genesys Cloud Platform Client.
Args:
env: The Genesys Cloud environment (e.g., 'us-east-1', 'eu-west-1').
client_id: Your OAuth Client ID.
client_secret: Your OAuth Client Secret.
Returns:
An authenticated PureCloudPlatformClientV2 instance.
"""
if not client_id or not client_secret:
raise ValueError("Client ID and Client Secret are required.")
# Initialize the client
client = PureCloudPlatformClientV2()
# Configure the environment
client.set_environment(env)
# Set the credentials
client.set_credentials(client_id, client_secret)
return client
Note: The SDK caches the access token. When the token expires, the SDK automatically requests a new one using the stored client credentials. You do not need to implement manual refresh logic in your application code.
Implementation
The /api/v2/analytics/conversations/details/query endpoint is a cursor-based pagination endpoint, not a traditional offset-based page endpoint. This distinction is critical.
In offset-based pagination, you request page=2 and size=100. In cursor-based pagination, the response contains a nextPageCursor string. You must pass this string back in the pageCursor parameter of your subsequent request to get the next set of results.
Step 1: Constructing the Initial Query
You must define an AnalyticsConversationDetailQuery object. This object specifies the time range, entity filters (users, queues, skills), and the metrics you want to retrieve.
Critical Constraint: The time range for this endpoint cannot exceed 30 days. If you need data older than 30 days, you must make multiple non-overlapping requests for different 30-day windows.
from genesyscloud.analytics.models import AnalyticsConversationDetailQuery
from datetime import datetime, timezone
def build_query_request(
start_time: datetime,
end_time: datetime,
entity_ids: list[str] = None
) -> AnalyticsConversationDetailQuery:
"""
Build the AnalyticsConversationDetailQuery object for the initial request.
Args:
start_time: ISO 8601 start time (must be within last 30 days).
end_time: ISO 8601 end time.
entity_ids: Optional list of user IDs or queue IDs to filter by.
Returns:
Configured AnalyticsConversationDetailQuery object.
"""
query = AnalyticsConversationDetailQuery()
# Set the time range
query.start_time = start_time.isoformat()
query.end_time = end_time.isoformat()
# Set the granularity (e.g., 'hour', 'day', 'week', 'month')
# 'day' is common for high-level reporting
query.granularity = "day"
# Define the entities to include
entities = []
if entity_ids:
for eid in entity_ids:
# Assuming these are User IDs for this example
entities.append({
"type": "user",
"id": eid
})
query.entities = entities
# Define the metrics you want
# Common metrics: handleDuration, talkDuration, waitDuration
query.metrics = [
"handleDuration",
"talkDuration",
"waitDuration",
"holdDuration"
]
return query
Step 2: Executing the First Request and Handling the Response
You use the AnalyticsApi from the SDK. The post_analytics_conversations_details_query method sends the POST request.
The response object contains:
totalRecords: The total number of records matching your query (useful for progress tracking, but not for pagination logic).pageSize: The number of records returned in this specific batch.nextPageCursor: A string token. If this isNoneor empty, you have reached the end of the data. If it exists, you must use it for the next request.
from genesyscloud.analytics.api import AnalyticsApi
from genesyscloud.rest import ApiException
def fetch_first_page(
client: PureCloudPlatformClientV2,
query_body: AnalyticsConversationDetailQuery
) -> tuple:
"""
Fetch the first page of analytics data.
Args:
client: Authenticated platform client.
query_body: The query object built in Step 1.
Returns:
A tuple of (response_data, next_page_cursor).
If no more data, next_page_cursor is None.
"""
analytics_api = AnalyticsApi(client)
try:
# Execute the query
response = analytics_api.post_analytics_conversations_details_query(
body=query_body
)
# Extract the cursor for the next iteration
next_cursor = response.next_page_cursor
# Return the data and the cursor
return response, next_cursor
except ApiException as e:
print(f"Exception when calling AnalyticsApi->post_analytics_conversations_details_query: {e}\n")
raise
Step 3: Iterating Through Pages Using the Cursor
This is the core of the pagination logic. You create a loop that continues as long as next_page_cursor is not None. In each iteration, you modify the query object to include the page_cursor and re-submit the request.
Important: You must clone or reset the query object appropriately. The SDK models are mutable. It is safer to pass the page_cursor directly in the API call if the SDK supports it, or update the query object. In the Genesys Cloud Python SDK, the post_analytics_conversations_details_query method accepts a page_cursor keyword argument.
def iterate_all_pages(
client: PureCloudPlatformClientV2,
initial_query: AnalyticsConversationDetailQuery
) -> list:
"""
Retrieve all pages of conversation details using cursor-based pagination.
Args:
client: Authenticated platform client.
initial_query: The initial query object without a page cursor.
Returns:
A list of all conversation detail records.
"""
analytics_api = AnalyticsApi(client)
all_records = []
# Start with the initial query (no cursor)
current_cursor = None
while True:
try:
# Call the API, passing the cursor if it exists
response = analytics_api.post_analytics_conversations_details_query(
body=initial_query,
page_cursor=current_cursor
)
# Append the records from this page to our accumulator
if response.entities and len(response.entities) > 0:
all_records.extend(response.entities)
# Check if there is a next page
next_cursor = response.next_page_cursor
if not next_cursor:
# No more pages, exit the loop
break
# Prepare for the next iteration
current_cursor = next_cursor
# Optional: Add a small delay to be respectful of rate limits
# if processing very large datasets
# import time
# time.sleep(0.1)
except ApiException as e:
print(f"Error during pagination: {e}\n")
# Handle specific errors like 429 Too Many Requests here
if e.status == 429:
print("Rate limited. Waiting 5 seconds before retry...")
import time
time.sleep(5)
continue # Retry the same page
else:
break # Stop on other errors
return all_records
Complete Working Example
This script combines authentication, query construction, and pagination into a single runnable module. It retrieves conversation details for the last 7 days.
import os
import sys
from datetime import datetime, timedelta, timezone
from genesyscloud import PureCloudPlatformClientV2
from genesyscloud.analytics.api import AnalyticsApi
from genesyscloud.analytics.models import AnalyticsConversationDetailQuery
from genesyscloud.rest import ApiException
def get_platform_client() -> PureCloudPlatformClientV2:
"""Initialize the Genesys Cloud Platform Client."""
client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
env = os.getenv("GENESYS_CLOUD_ENV", "us-east-1")
if not client_id or not client_secret:
raise ValueError("Please set GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET environment variables.")
client = PureCloudPlatformClientV2()
client.set_environment(env)
client.set_credentials(client_id, client_secret)
return client
def main():
try:
# 1. Authenticate
print("Authenticating with Genesys Cloud...")
client = get_platform_client()
# 2. Define Time Range (Last 7 Days)
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(days=7)
# Ensure ISO 8601 format with timezone
start_iso = start_time.isoformat()
end_iso = end_time.isoformat()
print(f"Querying data from {start_iso} to {end_iso}")
# 3. Build Query
query = AnalyticsConversationDetailQuery()
query.start_time = start_iso
query.end_time = end_iso
query.granularity = "day"
# Example: Filter by a specific User ID
# You must replace 'YOUR_USER_ID_HERE' with a valid UUID
user_id = os.getenv("GENESYS_CLOUD_USER_ID")
if user_id:
query.entities = [
{
"type": "user",
"id": user_id
}
]
print(f"Filtering by User ID: {user_id}")
else:
print("No User ID provided. Querying all users in the organization.")
# Define metrics
query.metrics = [
"handleDuration",
"talkDuration",
"waitDuration",
"holdDuration",
"wrapupDuration"
]
# 4. Execute Pagination
analytics_api = AnalyticsApi(client)
all_records = []
current_cursor = None
page_count = 0
print("Starting pagination loop...")
while True:
try:
# Submit the query with the current cursor
response = analytics_api.post_analytics_conversations_details_query(
body=query,
page_cursor=current_cursor
)
# Accumulate data
if response.entities:
all_records.extend(response.entities)
page_count += 1
print(f"Page {page_count}: Retrieved {len(response.entities)} records. Total so far: {len(all_records)}")
# Check for next page
if not response.next_page_cursor:
print("No more pages. Pagination complete.")
break
# Update cursor for next iteration
current_cursor = response.next_page_cursor
except ApiException as e:
print(f"API Exception: {e.status} - {e.reason}")
if e.status == 429:
print("Rate limited. Retrying in 5 seconds...")
import time
time.sleep(5)
continue
else:
print("Fatal error. Stopping.")
break
# 5. Process Results
print(f"\nTotal records retrieved: {len(all_records)}")
if all_records:
print("\nSample Record:")
# Print the first record's key fields
first_rec = all_records[0]
print(f" Conversation ID: {first_rec.conversation_id}")
print(f" Start Time: {first_rec.start_time}")
print(f" End Time: {first_rec.end_time}")
if first_rec.metrics:
print(f" Handle Duration (ms): {first_rec.metrics.get('handleDuration', {}).get('total', 0)}")
except Exception as e:
print(f"Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 400 Bad Request - Invalid Time Range
Cause: The start_time and end_time difference exceeds 30 days, or the start_time is in the future.
Fix: Ensure your time range is within the last 30 days. If you need historical data, split your query into multiple 30-day chunks.
# Correct: 7-day window
start_time = datetime.now(timezone.utc) - timedelta(days=7)
end_time = datetime.now(timezone.utc)
# Incorrect: 60-day window (Will fail)
# start_time = datetime.now(timezone.utc) - timedelta(days=60)
Error: 401 Unauthorized or 403 Forbidden
Cause: Missing or incorrect OAuth scopes, or invalid client credentials.
Fix: Verify that your OAuth Client has the analytics:conversation:details:view scope assigned in the Genesys Cloud Admin Console under Security > OAuth Clients.
Error: 429 Too Many Requests
Cause: You are hitting the rate limit for the Analytics API. Genesys Cloud enforces rate limits per client ID.
Fix: Implement exponential backoff. The example above includes a simple 5-second retry for 429s. For production, use a library like tenacity for robust retry logic.
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=2, max=30))
def resilient_query_call(analytics_api, query, cursor):
return analytics_api.post_analytics_conversations_details_query(
body=query,
page_cursor=cursor
)
Error: Empty Response with No Cursor
Cause: Your query filters (e.g., specific User ID or Queue ID) do not match any conversations in the specified time range.
Fix: Broaden your filters. Remove the entities filter to see if any data exists in the time range. Check the totalRecords field in the response; if it is 0, no data matches your criteria.