Building Queue and Media Type Aggregations with the Genesys Cloud Analytics API
What You Will Build
- You will construct a programmatic query that retrieves conversation statistics grouped by specific queues and media types (voice, chat, email).
- This tutorial uses the Genesys Cloud PureCloud Platform Client V2 SDK and the underlying REST API.
- The code examples are provided in Python 3.9+ and JavaScript (Node.js 18+).
Prerequisites
Before writing code, you must configure your environment with the following requirements:
- OAuth Client Credentials: You need a Genesys Cloud OAuth 2.0 client ID and secret. The client must have the
analytics:query:readscope. - SDK Installation:
- Python:
pip install purecloud-platform-client-v2 - Node.js:
npm install @genesys/purecloud-platform-client-v2
- Python:
- Runtime: Python 3.9+ or Node.js 18+.
- Organization ID: Your Genesys Cloud organization ID is not required for the API call itself but is implicit in the OAuth token.
Authentication Setup
The Genesys Cloud SDKs handle OAuth 2.0 Client Credentials flows automatically. You must initialize the client with your environment, client ID, and client secret. The SDK manages token caching and automatic refresh.
Python Initialization
from purecloudplatformclientv2 import PlatformClient
from purecloudplatformclientv2.analytics_api import AnalyticsApi
def init_analytics_client(client_id: str, client_secret: str, environment: str = "mypurecloud.com") -> AnalyticsApi:
"""
Initializes the PureCloud Platform Client and returns the Analytics API instance.
"""
platform_client = PlatformClient(
client_id=client_id,
client_secret=client_secret,
environment=environment
)
# The SDK handles token acquisition and refresh internally
return platform_client.analytics_api
JavaScript Initialization
const { PlatformClient, AnalyticsApi } = require('@genesys/purecloud-platform-client-v2');
async function initAnalyticsClient(clientId, clientSecret, environment = 'mypurecloud.com') {
const platformClient = new PlatformClient({
clientId,
clientSecret,
environment
});
// Initialize the client to ensure tokens are available
await platformClient.ready();
return new AnalyticsApi(platformClient);
}
Implementation
The core of this task is the POST /api/v2/analytics/conversations/details/query endpoint. Unlike simple GET requests, this endpoint requires a structured JSON body defining the metrics, dimensions, and time window.
Step 1: Constructing the Query Body
The query body consists of three main sections: interval, metrics, and groupBys.
- Interval: Defines the start and end time of the data window. It uses ISO 8601 format with timezone offsets.
- Metrics: An array of metric names. For queue analytics, common metrics include
offerCount,answerCount,abandonCount, andwaitTime. - GroupBys: An array of dimension names. To group by queue and media type, you must include
queueandmediaTypein this array.
Important: The mediaType dimension is only valid if the query scope includes media types that support it. If you query only voice, mediaType will always be “voice”. To see varied data, ensure your date range captures activity across voice, chat, and email.
Python Query Construction
from datetime import datetime, timezone, timedelta
import json
def build_query_body(start_time: datetime, end_time: datetime) -> dict:
"""
Constructs the JSON body for the analytics query.
Args:
start_time: Start of the reporting period (UTC).
end_time: End of the reporting period (UTC).
Returns:
A dictionary representing the query payload.
"""
# Ensure times are in UTC with timezone info
if start_time.tzinfo is None:
start_time = start_time.replace(tzinfo=timezone.utc)
if end_time.tzinfo is None:
end_time = end_time.replace(tzinfo=timezone.utc)
query = {
"interval": f"{start_time.isoformat()}/{end_time.isoformat()}",
"metrics": [
"offerCount",
"answerCount",
"abandonCount",
"waitTime",
"handleTime"
],
"groupBys": [
"queue",
"mediaType"
],
"view": "realtime" # Use "standard" for historical data
}
return query
JavaScript Query Construction
function buildQueryBody(startTime, endTime) {
// Ensure ISO strings include timezone offset
const startIso = startTime.toISOString();
const endIso = endTime.toISOString();
return {
interval: `${startIso}/${endIso}`,
metrics: [
"offerCount",
"answerCount",
"abandonCount",
"waitTime",
"handleTime"
],
groupBys: [
"queue",
"mediaType"
],
view: "standard" // Use "standard" for historical data
};
}
Step 2: Executing the Query
Once the body is constructed, you send it to the queryConversationsDetails method. The response is a ConversationsDetailsQueryResponse object.
Python Execution
def execute_query(analytics_api: AnalyticsApi, query_body: dict):
"""
Executes the analytics query and handles potential errors.
"""
try:
# The SDK method name corresponds to the API endpoint
response = analytics_api.query_conversations_details(body=query_body)
if response.body:
return response.body
else:
print("No data returned in response body.")
return None
except Exception as e:
# The SDK wraps HTTP errors. Check for 400 (Bad Request) or 401/403 (Auth)
print(f"Error executing query: {e}")
if hasattr(e, 'status'):
print(f"HTTP Status Code: {e.status}")
raise
JavaScript Execution
async function executeQuery(analyticsApi, queryBody) {
try {
const response = await analyticsApi.queryConversationsDetails({
body: queryBody
});
if (response.body) {
return response.body;
} else {
console.log("No data returned in response body.");
return null;
}
} catch (error) {
console.error("Error executing query:", error);
if (error.statusCode) {
console.error(`HTTP Status Code: ${error.statusCode}`);
}
throw error;
}
}
Step 3: Processing the Results
The response object contains an entities array. Each entity represents a unique combination of the groupBys dimensions (Queue ID/Name and Media Type).
Key Fields in the Response Entity:
queue: An object containingidandnameof the queue.mediaType: A string representing the media type (e.g., “voice”, “chat”, “email”).metrics: An object where keys are metric names and values are the aggregated numbers.
Python Result Processing
def process_results(response_body: dict):
"""
Iterates through the response entities and prints formatted data.
"""
if not response_body or 'entities' not in response_body:
print("No entities found in response.")
return
entities = response_body['entities']
print(f"Total entities returned: {len(entities)}")
print(f"{'Queue Name':<25} | {'Media Type':<10} | {'Offers':<8} | {'Answers':<8} | {'Abandons':<8}")
print("-" * 70)
for entity in entities:
queue_name = entity.get('queue', {}).get('name', 'Unknown Queue')
media_type = entity.get('mediaType', 'Unknown')
metrics = entity.get('metrics', {})
offers = metrics.get('offerCount', 0)
answers = metrics.get('answerCount', 0)
abandons = metrics.get('abandonCount', 0)
print(f"{queue_name:<25} | {media_type:<10} | {offers:<8} | {answers:<8} | {abandons:<8}")
JavaScript Result Processing
function processResults(responseBody) {
if (!responseBody || !responseBody.entities) {
console.log("No entities found in response.");
return;
}
const entities = responseBody.entities;
console.log(`Total entities returned: ${entities.length}`);
console.log(`Queue Name | Media Type | Offers | Answers | Abandons`);
console.log("-".repeat(70));
for (const entity of entities) {
const queueName = entity.queue?.name || 'Unknown Queue';
const mediaType = entity.mediaType || 'Unknown';
const metrics = entity.metrics || {};
const offers = metrics.offerCount || 0;
const answers = metrics.answerCount || 0;
const abandons = metrics.abandonCount || 0;
console.log(`${padLeft(queueName, 25)} | ${padLeft(mediaType, 10)} | ${padLeft(offers, 8)} | ${padLeft(answers, 8)} | ${padLeft(abandons, 8)}`);
}
}
function padLeft(str, len) {
return String(str).padStart(len, ' ');
}
Complete Working Example
Below is the complete, runnable Python script. Replace the placeholders YOUR_CLIENT_ID, YOUR_CLIENT_SECRET, and YOUR_ENVIRONMENT with your actual credentials.
import sys
import os
from datetime import datetime, timezone, timedelta
from purecloudplatformclientv2 import PlatformClient
from purecloudplatformclientv2.analytics_api import AnalyticsApi
# Configuration
CLIENT_ID = os.getenv("GENESYS_CLIENT_ID", "YOUR_CLIENT_ID")
CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET", "YOUR_CLIENT_SECRET")
ENVIRONMENT = os.getenv("GENESYS_ENVIRONMENT", "mypurecloud.com")
def main():
# 1. Initialize the Client
print("Initializing PureCloud Platform Client...")
try:
platform_client = PlatformClient(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
environment=ENVIRONMENT
)
analytics_api = platform_client.analytics_api
except Exception as e:
print(f"Failed to initialize client: {e}")
sys.exit(1)
# 2. Define Time Window
# Query the last 24 hours
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(hours=24)
print(f"Querying data from {start_time.isoformat()} to {end_time.isoformat()}")
# 3. Construct Query Body
query_body = {
"interval": f"{start_time.isoformat()}/{end_time.isoformat()}",
"metrics": [
"offerCount",
"answerCount",
"abandonCount",
"waitTime",
"handleTime"
],
"groupBys": [
"queue",
"mediaType"
],
"view": "standard"
}
# 4. Execute Query
print("Executing analytics query...")
try:
response = analytics_api.query_conversations_details(body=query_body)
if response.body:
process_results(response.body)
else:
print("No data returned. Check your date range and permissions.")
except Exception as e:
print(f"Error executing query: {e}")
if hasattr(e, 'status'):
print(f"HTTP Status Code: {e.status}")
if hasattr(e, 'body'):
print(f"Response Body: {e.body}")
def process_results(response_body: dict):
if not response_body or 'entities' not in response_body:
print("No entities found in response.")
return
entities = response_body['entities']
print(f"Total entities returned: {len(entities)}")
print(f"{'Queue Name':<25} | {'Media Type':<10} | {'Offers':<8} | {'Answers':<8} | {'Abandons':<8}")
print("-" * 70)
for entity in entities:
# Safely access nested fields
queue_info = entity.get('queue', {})
queue_name = queue_info.get('name', 'Unknown Queue') if queue_info else 'Unknown Queue'
media_type = entity.get('mediaType', 'Unknown')
metrics = entity.get('metrics', {})
offers = metrics.get('offerCount', 0)
answers = metrics.get('answerCount', 0)
abandons = metrics.get('abandonCount', 0)
# Format output
print(f"{queue_name:<25} | {media_type:<10} | {offers:<8} | {answers:<8} | {abandons:<8}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 400 Bad Request - Invalid Interval
Cause: The interval parameter is malformed or the time range is too large. The Genesys Cloud Analytics API has limits on the time window size for certain views. For the standard view, the maximum interval is typically 30 days. For realtime, it is much smaller.
Fix: Ensure your start and end times are in valid ISO 8601 format with timezone offsets. Verify the duration is within limits.
# Correct format
"interval": "2023-10-01T00:00:00+00:00/2023-10-02T00:00:00+00:00"
Error: 401 Unauthorized or 403 Forbidden
Cause: The OAuth token is expired, invalid, or lacks the analytics:query:read scope.
Fix:
- Verify your client ID and secret are correct.
- Ensure the OAuth application in Genesys Cloud has the
analytics:query:readscope assigned. - If using the SDK, the token refresh is automatic. If you are managing tokens manually, implement a refresh loop.
Error: Empty Entities Array
Cause: No conversation data exists for the specified queues and media types in the given time window.
Fix:
- Broaden the time window.
- Verify that the queues included in your organization actually had activity during the selected period.
- Check if
mediaTypefiltering is inadvertently excluding all data. If you group bymediaType, ensure you are not also filtering by a specific media type in awhereclause that conflicts with your expectations.
Error: 429 Too Many Requests
Cause: You have exceeded the rate limit for the Analytics API.
Fix: Implement exponential backoff. The Genesys Cloud SDK does not automatically retry 429s in all languages. You must catch the error and retry after a delay.
import time
def execute_query_with_retry(analytics_api, query_body, max_retries=3):
for attempt in range(max_retries):
try:
response = analytics_api.query_conversations_details(body=query_body)
return response.body
except Exception as e:
if hasattr(e, 'status') and e.status == 429:
wait_time = 2 ** attempt # Exponential backoff: 2s, 4s, 8s
print(f"Rate limited. Retrying in {wait_time} seconds...")
time.sleep(wait_time)
else:
raise
raise Exception("Max retries exceeded for 429 error")