Calculate Service Level Percentage Using Raw Analytics API Interval Data

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 queue interval 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-sdk
    • pip 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:

  1. interactions.offered: Total interactions offered to the queue.
  2. interactions.answered: Total interactions answered.
  3. 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.offered is 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.answeredWithinThreshold is the numerator.
  • interactions.offered is 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:

  1. The queue has no service level threshold configured in the Admin Console.
  2. The time window has no traffic.
  3. 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.

Official References