Calculate Service Level Percentage Using Raw Analytics API Interval Data
What You Will Build
- You will build a script that queries the Genesys Cloud Analytics API for raw interval data and calculates the Service Level percentage (percentage of interactions answered within a defined threshold).
- This solution uses the Genesys Cloud Python SDK (
genesys-cloud-sdk) to handle authentication, pagination, and data retrieval. - The tutorial covers Python 3.9+ and demonstrates the mathematical logic required to derive Service Level from
queueinterval metrics.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth client with the following scopes:
analytics:query:read(Required for querying analytics data)
- SDK Version:
genesys-cloud-sdk>= 150.0.0 - Language/Runtime: Python 3.9 or higher
- External Dependencies:
pip install genesys-cloud-sdkpip install python-dotenv(For secure credential management)
Authentication Setup
Genesys Cloud uses OAuth 2.0 for authentication. The Python SDK handles the token exchange automatically when initialized with client credentials. You must store your Client ID and Client Secret securely. Never hardcode these values.
Create a .env file in your project root:
GENESYS_CLIENT_ID=your_client_id_here
GENESYS_CLIENT_SECRET=your_client_secret_here
GENESYS_REGION=us-east-1
Initialize the client using the PureCloudPlatformClientV2 class. This object manages the session and token refresh logic.
import os
from dotenv import load_dotenv
from purecloud_platform_client import PureCloudPlatformClientV2, Configuration
# Load environment variables
load_dotenv()
def get_platform_client() -> PureCloudPlatformClientV2:
"""
Initializes and returns a configured Genesys Cloud platform client.
"""
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
region = os.getenv("GENESYS_REGION", "us-east-1")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in environment variables.")
# Construct the API host based on region
# Note: The SDK often handles this internally, but explicit configuration is safer for custom regions.
api_host = f"https://api.{region}.mypurecloud.com" if region != "us-east-1" else "https://api.mypurecloud.com"
configuration = Configuration(
client_id=client_id,
client_secret=client_secret,
api_host=api_host
)
return PureCloudPlatformClientV2(configuration)
Implementation
Step 1: Construct the Analytics Query Body
To calculate Service Level, you need interval data from the Analytics API. The endpoint /api/v2/analytics/queues/details/query returns aggregated metrics over time intervals.
Service Level is defined as:
$$ \text{Service Level %} = \left( \frac{\text{Interactions Answered Within Threshold}}{\text{Total Interactions Offered}} \right) \times 100 $$
In Genesys Cloud analytics, you do not query a single “Service Level” field for raw calculation. Instead, you query:
interactions.offered: Total interactions offered to the queue.interactions.answered: Total interactions answered.interactions.answeredWithinThreshold: Interactions answered within the service level threshold (e.g., 20 seconds).
You must specify the metricIds in the request body. The groupBy parameter should be set to interval to get data points over time.
from purecloud_platform_client.rest import ApiException
from datetime import datetime, timedelta
def build_analytics_query_body(queue_id: str, start_time: datetime, end_time: datetime) -> dict:
"""
Constructs the request body for the Analytics Queues Details Query.
Args:
queue_id: The ID of the queue to analyze.
start_time: Start of the query window.
end_time: End of the query window.
Returns:
A dictionary representing the JSON payload for the API call.
"""
# Define the metrics required for Service Level calculation
metric_ids = [
"interactions.offered",
"interactions.answered",
"interactions.answeredWithinThreshold"
]
# Format dates to ISO 8601 format with timezone (UTC)
start_iso = start_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
end_iso = end_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
query_body = {
"dateFrom": start_iso,
"dateTo": end_iso,
"groupBy": ["interval"],
"metricIds": metric_ids,
"filter": {
"and": [
{
"dimension": "queueId",
"operator": "eq",
"value": queue_id
}
]
},
# Interval size in seconds. 300 seconds (5 minutes) is standard for reporting.
# Smaller intervals (e.g., 60) provide more granularity but increase payload size.
"interval": 300
}
return query_body
Step 2: Execute the Query and Handle Pagination
The Analytics API returns paginated results. You must iterate through all pages to ensure you capture every interval in the selected timeframe. Failing to handle pagination will result in inaccurate totals.
The SDK method post_analytics_queues_details_query handles the HTTP request. You must catch ApiException for errors like 401 (Unauthorized) or 429 (Rate Limited).
def fetch_queue_analytics_data(client: PureCloudPlatformClientV2, query_body: dict) -> list:
"""
Fetches all pages of analytics data for the given query body.
Args:
client: The initialized PureCloudPlatformClientV2 instance.
query_body: The dictionary containing the query parameters.
Returns:
A list of all interval data objects from all pages.
"""
analytics_api = client.analytics_api
all_intervals = []
page_token = None
try:
while True:
# Execute the query
# The SDK automatically serializes the dictionary to JSON
response = analytics_api.post_analytics_queues_details_query(body=query_body, page_token=page_token)
# Append the intervals from this page
if response.entities:
all_intervals.extend(response.entities)
# Check if there are more pages
if response.next_page_token:
page_token = response.next_page_token
else:
break
except ApiException as e:
print(f"Exception when calling AnalyticsApi->post_analytics_queues_details_query: {e}")
if e.status == 401:
raise RuntimeError("Authentication failed. Check Client ID and Secret.")
elif e.status == 403:
raise RuntimeError("Forbidden. Ensure the client has 'analytics:query:read' scope.")
elif e.status == 429:
raise RuntimeError("Rate limit exceeded. Implement exponential backoff.")
else:
raise
except Exception as e:
print(f"Unexpected error: {e}")
raise
return all_intervals
Step 3: Calculate Service Level from Raw Data
Once you have the list of interval objects, you must aggregate the metrics. Each interval object contains a metrics dictionary. You must sum the values for the three key metrics across all intervals.
Critical Logic:
- If
interactions.offeredis 0, Service Level is undefined (or 100% depending on business rule, but mathematically it is 0/0). The code below handles this by returning 0.0 or skipping the interval. interactions.answeredWithinThresholdis the numerator.interactions.offeredis the denominator.
def calculate_service_level(intervals: list, threshold_seconds: int = 20) -> dict:
"""
Calculates the aggregate Service Level percentage from a list of interval data.
Args:
intervals: List of analytics interval objects.
threshold_seconds: The service level threshold in seconds (e.g., 20).
Note: The API returns answeredWithinThreshold based on the
queue's configured SL threshold, not this parameter.
This parameter is for documentation/validation purposes.
Returns:
A dictionary containing total offered, total answered, total within threshold,
and the calculated service level percentage.
"""
total_offered = 0
total_answered = 0
total_within_threshold = 0
for interval in intervals:
metrics = interval.metrics
# Extract metric values, defaulting to 0 if the metric is missing for an interval
offered = metrics.get("interactions.offered", {}).get("value", 0) or 0
answered = metrics.get("interactions.answered", {}).get("value", 0) or 0
within_threshold = metrics.get("interactions.answeredWithinThreshold", {}).get("value", 0) or 0
# Accumulate totals
total_offered += offered
total_answered += answered
total_within_threshold += within_threshold
# Calculate Service Level Percentage
if total_offered == 0:
service_level_pct = 0.0
else:
service_level_pct = (total_within_threshold / total_offered) * 100
return {
"total_offered": total_offered,
"total_answered": total_answered,
"total_within_threshold": total_within_threshold,
"service_level_percentage": round(service_level_pct, 2)
}
Complete Working Example
This script combines the steps above into a single executable module. It queries the last 24 hours of data for a specific queue and prints the calculated Service Level.
import os
import sys
from datetime import datetime, timedelta
from dotenv import load_dotenv
from purecloud_platform_client import PureCloudPlatformClientV2, Configuration
from purecloud_platform_client.rest import ApiException
# Load environment variables
load_dotenv()
def get_platform_client() -> PureCloudPlatformClientV2:
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
region = os.getenv("GENESYS_REGION", "us-east-1")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in environment variables.")
api_host = f"https://api.{region}.mypurecloud.com" if region != "us-east-1" else "https://api.mypurecloud.com"
configuration = Configuration(
client_id=client_id,
client_secret=client_secret,
api_host=api_host
)
return PureCloudPlatformClientV2(configuration)
def build_analytics_query_body(queue_id: str, start_time: datetime, end_time: datetime) -> dict:
metric_ids = [
"interactions.offered",
"interactions.answered",
"interactions.answeredWithinThreshold"
]
start_iso = start_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
end_iso = end_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
query_body = {
"dateFrom": start_iso,
"dateTo": end_iso,
"groupBy": ["interval"],
"metricIds": metric_ids,
"filter": {
"and": [
{
"dimension": "queueId",
"operator": "eq",
"value": queue_id
}
]
},
"interval": 300 # 5-minute intervals
}
return query_body
def fetch_queue_analytics_data(client: PureCloudPlatformClientV2, query_body: dict) -> list:
analytics_api = client.analytics_api
all_intervals = []
page_token = None
try:
while True:
response = analytics_api.post_analytics_queues_details_query(body=query_body, page_token=page_token)
if response.entities:
all_intervals.extend(response.entities)
if response.next_page_token:
page_token = response.next_page_token
else:
break
except ApiException as e:
print(f"API Exception: {e}")
if e.status == 429:
print("Rate limited. Please retry after a delay.")
raise
except Exception as e:
print(f"Unexpected error: {e}")
raise
return all_intervals
def calculate_service_level(intervals: list) -> dict:
total_offered = 0
total_answered = 0
total_within_threshold = 0
for interval in intervals:
metrics = interval.metrics
offered = metrics.get("interactions.offered", {}).get("value", 0) or 0
answered = metrics.get("interactions.answered", {}).get("value", 0) or 0
within_threshold = metrics.get("interactions.answeredWithinThreshold", {}).get("value", 0) or 0
total_offered += offered
total_answered += answered
total_within_threshold += within_threshold
if total_offered == 0:
service_level_pct = 0.0
else:
service_level_pct = (total_within_threshold / total_offered) * 100
return {
"total_offered": total_offered,
"total_answered": total_answered,
"total_within_threshold": total_within_threshold,
"service_level_percentage": round(service_level_pct, 2)
}
def main():
# Configuration
# Replace this with your actual Queue ID
QUEUE_ID = os.getenv("GENESYS_QUEUE_ID", "your-queue-id-here")
if QUEUE_ID == "your-queue-id-here":
print("Error: Set GENESYS_QUEUE_ID in your .env file.")
sys.exit(1)
# Time window: Last 24 hours
end_time = datetime.utcnow()
start_time = end_time - timedelta(hours=24)
print(f"Fetching analytics for Queue: {QUEUE_ID}")
print(f"Time Window: {start_time} to {end_time}")
try:
client = get_platform_client()
query_body = build_analytics_query_body(QUEUE_ID, start_time, end_time)
intervals = fetch_queue_analytics_data(client, query_body)
if not intervals:
print("No data found for the specified queue and time range.")
return
results = calculate_service_level(intervals)
print("\n--- Service Level Report ---")
print(f"Total Interactions Offered: {results['total_offered']}")
print(f"Total Interactions Answered: {results['total_answered']}")
print(f"Total Answered Within Threshold: {results['total_within_threshold']}")
print(f"Service Level Percentage: {results['service_level_percentage']}%")
except Exception as e:
print(f"Fatal error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
Cause: The Client ID or Client Secret is invalid, or the OAuth token has expired and the SDK failed to refresh it.
Fix: Verify the credentials in your .env file. Ensure the client is active in the Genesys Cloud Admin Console.
Code Check:
if e.status == 401:
raise RuntimeError("Authentication failed. Check Client ID and Secret.")
Error: 403 Forbidden
Cause: The OAuth client lacks the analytics:query:read scope.
Fix: Go to Admin > Security > OAuth 2.0 Clients. Edit your client and add the analytics:query:read scope. Save and regenerate credentials if necessary.
Code Check:
if e.status == 403:
raise RuntimeError("Forbidden. Ensure the client has 'analytics:query:read' scope.")
Error: 429 Too Many Requests
Cause: You have exceeded the Genesys Cloud API rate limits. Analytics queries are heavy and consume more quota than standard CRUD operations.
Fix: Implement exponential backoff. Do not retry immediately.
Code Example:
import time
def retry_with_backoff(func, *args, max_retries=3, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except ApiException as e:
if e.status == 429:
wait_time = 2 ** attempt # 1, 2, 4 seconds
print(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time)
else:
raise
raise Exception("Max retries exceeded")
Error: interactions.answeredWithinThreshold is 0 or Null
Cause:
- The queue has no service level threshold configured in the Admin Console.
- The time window has no traffic.
- The metric ID is misspelled.
Fix: Verify the queue configuration in Genesys Cloud Admin. Ensure “Service Level” is set (e.g., 20 seconds). If the queue has no SL configured, the API may not populate this metric accurately.