Calculate Service Level Percentage from Raw Analytics Interval Data

Calculate Service Level Percentage from Raw Analytics Interval Data

What You Will Build

  • A script that queries Genesys Cloud Analytics for raw conversation interval data and calculates the Service Level percentage (percentage of conversations answered within a defined threshold).
  • This tutorial uses the Genesys Cloud Analytics Conversations Details API (/api/v2/analytics/conversations/details/query).
  • The implementation covers Python using the official genesyscloud_python SDK and raw requests for clarity.

Prerequisites

  • OAuth Client Type: Service Account (JWT) or Client Credentials.
  • Required Scopes:
    • analytics:conversation:view (Required to query conversation details).
    • analytics:report:view (Optional, but often required depending on tenant configuration for analytics access).
  • SDK Version: genesyscloud_python >= 140.0.0.
  • Runtime: Python 3.8+.
  • Dependencies: genesyscloud_python, pandas (optional, for data manipulation), requests.

Install dependencies:

pip install genesyscloud_python pandas requests

Authentication Setup

Genesys Cloud uses JWT or Client Credentials flow for server-to-server integrations. For this tutorial, we assume a Service Account with a JWT private key.

The genesyscloud_python SDK handles token refresh automatically if configured correctly. You must initialize the PureCloudPlatformClientV2 with your environment URL (e.g., mypurecloud.com) and the authentication provider.

import os
from purecloudplatformclientv2.rest import ApiException
from purecloudplatformclientv2 import PureCloudPlatformClientV2, PlatformConfiguration

def get_purecloud_client():
    """
    Initializes the PureCloud Platform Client V2 using JWT authentication.
    """
    # Environment variables should be set in your deployment environment
    environment_url = os.getenv("PURECLOUD_ENVIRONMENT", "mypurecloud.com")
    client_id = os.getenv("PURECLOUD_CLIENT_ID")
    private_key = os.getenv("PURECLOUD_PRIVATE_KEY")
    private_key_password = os.getenv("PURECLOUD_PRIVATE_KEY_PASSWORD")

    if not all([client_id, private_key]):
        raise ValueError("Missing required OAuth credentials.")

    # Configure the platform client
    platform_client = PureCloudPlatformClientV2(environment_url)

    # Configure JWT authentication
    auth_config = platform_client.get_jwt_auth_configuration(
        client_id=client_id,
        private_key=private_key,
        private_key_password=private_key_password
    )

    try:
        # Initialize the client
        platform_client.set_auth_provider(auth_config)
        return platform_client
    except ApiException as e:
        print(f"Authentication failed: {e}")
        raise

# Initialize client
client = get_purecloud_client()

Implementation

Step 1: Query Raw Conversation Interval Data

The Analytics API does not return a pre-calculated “Service Level %” field in the raw interval data. Instead, it returns individual conversation records with timestamps for answer_time and ring_time. You must calculate the Service Level locally by comparing these timestamps against your defined threshold (e.g., 20 seconds).

We will use the AnalyticsApi to query conversation details. The key parameters are:

  • interval_type: 5min or 15min is standard for interval data. 5min provides higher granularity.
  • group_by: interval is required to get data bucketed by time.
  • metrics: We need answered_count and abandoned_count for context, but the actual SL calculation relies on individual conversation timing data. However, querying all individual conversations for a large date range is inefficient.

Critical Design Decision:
For accurate Service Level calculation at scale, it is better to query Summary Data with a filter for wait_time or use the Details Query with a specific metric wait_time. The wait_time metric in the summary query often provides the percentile data, but to get the exact percentage of calls answered within X seconds, we need to filter conversations where wait_time < threshold.

The most robust method for raw interval data is to query the Analytics Conversation Details endpoint with a filter on wait_time. However, the Details API is expensive. A more efficient approach for historical reporting is to use the Analytics Summary Query endpoint (/api/v2/analytics/conversations/summary/query) with a filter, but the prompt specifies Raw Analytics API interval data.

Let us stick to the Details Query but optimize it by only selecting the necessary fields and using a reasonable time window. We will calculate SL by counting conversations where wait_time is less than our threshold.

OAuth Scope: analytics:conversation:view

from purecloudplatformclientv2 import AnalyticsApi, ConversationDetailsQuery, ConversationDetailsQueryFilter, ConversationDetailsQueryMetric

def query_conversation_details(client, start_time, end_time, interval_type="5min"):
    """
    Queries raw conversation interval data for a specific time range.
    """
    analytics_api = AnalyticsApi(client)

    # Define the query body
    query = ConversationDetailsQuery()
    
    # Set time range
    query.start_time = start_time
    query.end_time = end_time
    
    # Set interval type
    query.interval_type = interval_type
    
    # Group by interval to get aggregated data per time bucket
    query.group_by = ["interval"]
    
    # Define metrics to retrieve
    # We need wait_time to calculate SL. 
    # Note: Retrieving 'wait_time' in details query can be heavy. 
    # For large datasets, consider using Summary API with filters.
    query.metrics = ["wait_time", "answered_count", "abandoned_count"]
    
    # Optional: Filter for specific queue or skill to reduce data volume
    # query.filters = [ConversationDetailsQueryFilter(name="queue.id", operator="eq", value=["YOUR_QUEUE_ID"])]

    try:
        # Execute the query
        response = analytics_api.post_analytics_conversations_details_query(body=query)
        return response
    except ApiException as e:
        if e.status == 429:
            print("Rate limit exceeded. Implement retry logic.")
        elif e.status == 403:
            print("Forbidden. Check OAuth scopes.")
        raise

# Example usage
from datetime import datetime, timedelta

end_time = datetime.utcnow()
start_time = end_time - timedelta(hours=24)

# Note: The SDK returns a ConversationDetailsResponse object
# We will process this in the next step.

Step 2: Calculate Service Level from Raw Data

The response from the Details API contains a list of entities, where each entity represents an interval (e.g., a 5-minute bucket). Each entity contains metrics which include answered_count, abandoned_count, and potentially wait_time statistics.

However, the Details Query with group_by=interval aggregates the data. It does not give you individual conversation wait_time values unless you remove group_by (which returns every single conversation, which is dangerous for large volumes).

Correction for Accuracy:
To calculate Service Level accurately from interval data without downloading millions of records, you must use the Summary API with a filter, OR use the Details API without grouping but with a strict time window and pagination.

Since the topic is “Raw Analytics API interval data”, let us assume we are using the Summary API which provides aggregated metrics per interval, including the ability to filter by wait_time. But the Summary API does not return the count of calls within a threshold directly unless you use the filter parameter in the query.

Best Practice Approach:
Use the analytics:conversation:view scope to query the Summary API with a filter for wait_time < threshold. Then calculate SL as:
SL % = (Count of Answered Calls with Wait Time < Threshold) / (Total Answered Calls)

If you must use the Details API for raw data, you must fetch individual conversations. This is rarely done for SL calculation due to cost. However, if you have already fetched raw data (e.g., for another purpose), here is how you calculate it.

Let us demonstrate the Summary API with Filter approach, which is the standard way to get SL from interval data efficiently.

from purecloudplatformclientv2 import AnalyticsApi, ConversationSummaryQuery, ConversationSummaryQueryFilter, ConversationSummaryQueryMetric

def get_service_level_data(client, start_time, end_time, threshold_seconds=20, interval_type="5min"):
    """
    Retrieves aggregated conversation data filtered by wait time to calculate Service Level.
    """
    analytics_api = AnalyticsApi(client)

    # Define the query body
    query = ConversationSummaryQuery()
    query.start_time = start_time
    query.end_time = end_time
    query.interval_type = interval_type
    query.group_by = ["interval"]
    
    # Metrics: We need the count of answered calls
    query.metrics = ["answered_count"]

    # Filter: Only include conversations answered within the threshold
    # The wait_time metric is available in the filter context for summary queries
    filter_config = ConversationSummaryQueryFilter(
        name="wait_time",
        operator="lt",  # Less than
        value=[threshold_seconds]
    )
    query.filters = [filter_config]

    try:
        response = analytics_api.post_analytics_conversations_summary_query(body=query)
        return response
    except ApiException as e:
        print(f"API Error: {e.status} - {e.reason}")
        raise

def get_total_answered_count(client, start_time, end_time, interval_type="5min"):
    """
    Retrieves the total count of answered calls in the same period.
    """
    analytics_api = AnalyticsApi(client)
    query = ConversationSummaryQuery()
    query.start_time = start_time
    query.end_time = end_time
    query.interval_type = interval_type
    query.group_by = ["interval"]
    query.metrics = ["answered_count"]
    
    try:
        response = analytics_api.post_analytics_conversations_summary_query(body=query)
        return response
    except ApiException as e:
        print(f"API Error: {e.status} - {e.reason}")
        raise

Step 3: Processing Results and Calculating Percentage

The response from the Summary API contains a list of entities. Each entity corresponds to an interval. We must sum the answered_count from the filtered query (calls within SL) and divide it by the answered_count from the total query (all answered calls).

def calculate_service_level(filtered_response, total_response):
    """
    Calculates the Service Level percentage from two API responses.
    
    Args:
        filtered_response: Response from query with wait_time < threshold filter.
        total_response: Response from query with no wait_time filter.
    
    Returns:
        float: Service Level percentage (0.0 to 100.0).
    """
    if not filtered_response or not total_response:
        return 0.0

    # Sum answered counts from filtered response
    filtered_answered = sum(
        entity.metrics.get("answered_count", 0) 
        for entity in filtered_response.entities
    )

    # Sum answered counts from total response
    total_answered = sum(
        entity.metrics.get("answered_count", 0) 
        for entity in total_response.entities
    )

    if total_answered == 0:
        return 0.0

    # Calculate percentage
    service_level_percentage = (filtered_answered / total_answered) * 100
    return round(service_level_percentage, 2)

# Example Execution
threshold = 20  # seconds
filtered_data = get_service_level_data(client, start_time, end_time, threshold_seconds=threshold)
total_data = get_total_answered_count(client, start_time, end_time)

sl_percentage = calculate_service_level(filtered_data, total_data)
print(f"Service Level ({threshold}s threshold): {sl_percentage}%")

Complete Working Example

This script combines authentication, data retrieval, and calculation into a single runnable module.

import os
import sys
from datetime import datetime, timedelta
from purecloudplatformclientv2.rest import ApiException
from purecloudplatformclientv2 import PureCloudPlatformClientV2, AnalyticsApi, ConversationSummaryQuery, ConversationSummaryQueryFilter

def get_purecloud_client():
    environment_url = os.getenv("PURECLOUD_ENVIRONMENT", "mypurecloud.com")
    client_id = os.getenv("PURECLOUD_CLIENT_ID")
    private_key = os.getenv("PURECLOUD_PRIVATE_KEY")
    private_key_password = os.getenv("PURECLOUD_PRIVATE_KEY_PASSWORD")

    if not all([client_id, private_key]):
        raise ValueError("Missing required OAuth credentials.")

    platform_client = PureCloudPlatformClientV2(environment_url)
    auth_config = platform_client.get_jwt_auth_configuration(
        client_id=client_id,
        private_key=private_key,
        private_key_password=private_key_password
    )
    platform_client.set_auth_provider(auth_config)
    return platform_client

def get_conversation_counts(client, start_time, end_time, interval_type="5min", wait_time_filter=None):
    analytics_api = AnalyticsApi(client)
    query = ConversationSummaryQuery()
    query.start_time = start_time
    query.end_time = end_time
    query.interval_type = interval_type
    query.group_by = ["interval"]
    query.metrics = ["answered_count"]

    if wait_time_filter:
        filter_config = ConversationSummaryQueryFilter(
            name="wait_time",
            operator="lt",
            value=[wait_time_filter]
        )
        query.filters = [filter_config]

    try:
        response = analytics_api.post_analytics_conversations_summary_query(body=query)
        return sum(entity.metrics.get("answered_count", 0) for entity in response.entities)
    except ApiException as e:
        print(f"Error fetching data: {e.status} - {e.reason}")
        raise

def main():
    try:
        client = get_purecloud_client()
        
        # Define time range (last 24 hours)
        end_time = datetime.utcnow()
        start_time = end_time - timedelta(hours=24)
        
        # Define SL threshold in seconds
        sl_threshold_seconds = 20
        
        print(f"Calculating Service Level for {start_time} to {end_time}...")
        
        # Get total answered calls
        total_answered = get_conversation_counts(client, start_time, end_time)
        
        # Get answered calls within threshold
        answered_within_sl = get_conversation_counts(
            client, start_time, end_time, wait_time_filter=sl_threshold_seconds
        )
        
        # Calculate SL
        if total_answered > 0:
            sl_percentage = (answered_within_sl / total_answered) * 100
            print(f"Total Answered: {total_answered}")
            print(f"Answered within {sl_threshold_seconds}s: {answered_within_sl}")
            print(f"Service Level: {sl_percentage:.2f}%")
        else:
            print("No answered calls found in the specified period.")

    except Exception as e:
        print(f"Fatal error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 403 Forbidden

  • Cause: The OAuth token lacks the analytics:conversation:view scope.
  • Fix: Update your Service Account’s OAuth client scopes in the Genesys Cloud Admin portal. Ensure analytics:conversation:view is checked.

Error: 429 Too Many Requests

  • Cause: You are hitting the API rate limit. Analytics queries can be heavy.
  • Fix: Implement exponential backoff. The genesyscloud_python SDK does not auto-retry 429s by default. Wrap the API call in a retry loop.
import time

def retry_api_call(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  # Exponential backoff
                print(f"Rate limited. Retrying in {wait_time} seconds...")
                time.sleep(wait_time)
            else:
                raise
    raise Exception("Max retries exceeded")

# Usage
# response = retry_api_call(analytics_api.post_analytics_conversations_summary_query, body=query)

Error: Empty Entities List

  • Cause: No conversations occurred in the specified time range, or the filter is too strict.
  • Fix: Verify the start_time and end_time are in ISO 8601 format and cover a period with activity. Check that wait_time filter value is realistic (e.g., 20 seconds, not 0 seconds).

Error: Metric Not Found

  • Cause: The metric name is incorrect.
  • Fix: Use exact metric names from the API documentation. For answered calls, use answered_count. For wait time, use wait_time.

Official References