Calculate Service Level Percentage from Raw Genesys Cloud Analytics Interval Data

Calculate Service Level Percentage from Raw Genesys Cloud Analytics Interval Data

What You Will Build

  • You will write a script that queries the Genesys Cloud Analytics API for raw interval data, filters for offered and answered conversations, and computes the Service Level percentage for a specific time window.
  • This tutorial uses the Genesys Cloud /api/v2/analytics/conversations/details/query endpoint.
  • The implementation is provided in Python using the requests library.

Prerequisites

  • OAuth Client: A Genesys Cloud OAuth client with the analytics:query scope.
  • API Version: Genesys Cloud API v2.
  • Language/Runtime: Python 3.8+ with pip.
  • Dependencies:
    • requests: For HTTP calls.
    • python-dateutil: For parsing ISO 8601 dates.
    • pytz: For timezone handling.

Install the dependencies:

pip install requests python-dateutil pytz

Authentication Setup

Genesys Cloud uses OAuth 2.0 for authentication. You must obtain an access token before making API calls. The following function handles the client credentials flow.

import requests
import time
import json
from typing import Optional

GENESYS_CLOUD_REGION = "mypurecloud.com" # Change to your region, e.g., usw2.pure.cloud
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
OAUTH_TOKEN_URL = f"https://api.{GENESYS_CLOUD_REGION}/oauth/token"

def get_access_token() -> str:
    """
    Retrieves an OAuth access token using client credentials.
    In production, implement token caching to avoid refreshing on every call.
    """
    payload = {
        "grant_type": "client_credentials"
    }
    response = requests.post(
        OAUTH_TOKEN_URL,
        data=payload,
        auth=(CLIENT_ID, CLIENT_SECRET),
        headers={"Content-Type": "application/x-www-form-urlencoded"}
    )

    if response.status_code != 200:
        raise Exception(f"Failed to obtain access token: {response.text}")

    return response.json()["access_token"]

Implementation

Step 1: Define the Query Payload

The Analytics API requires a specific JSON structure to query conversation details. To calculate Service Level, you need two key metrics from the raw data:

  1. Offered: The total number of conversations that entered the queue.
  2. Answered: The number of conversations answered within the Service Level target (e.g., 20 seconds).

You must select the correct view. The conversation view provides aggregated data per interval.

def build_query_payload(
    start_date: str, 
    end_date: str, 
    queue_id: str, 
    service_level_seconds: int = 20
) -> dict:
    """
    Constructs the query payload for the analytics API.
    
    Args:
        start_date: ISO 8601 start date (e.g., "2023-10-01T00:00:00Z")
        end_date: ISO 8601 end date (e.g., "2023-10-01T23:59:59Z")
        queue_id: The ID of the queue to analyze.
        service_level_seconds: The threshold in seconds for SLA calculation.
    """
    return {
        "view": "conversation",
        "dateFrom": start_date,
        "dateTo": end_date,
        "groupBy": ["interval"],
        "interval": "PT1H", # Group by 1-hour intervals
        "filter": {
            "type": "and",
            "clauses": [
                {
                    "type": "field",
                    "path": "queue.id",
                    "operator": "eq",
                    "value": queue_id
                },
                {
                    "type": "field",
                    "path": "conversationType",
                    "operator": "eq",
                    "value": "voice" # Or "chat", "email", etc.
                }
            ]
        },
        "metrics": [
            "offered",
            "answered",
            "answerRate",
            "abandoned",
            "abandonedRate",
            "serviceLevel", # This metric is provided by API, but we will calculate it manually from raw counts for precision
            "longestQueuedDuration"
        ]
    }

Step 2: Execute the Query and Handle Pagination

The /api/v2/analytics/conversations/details/query endpoint returns paginated results. You must iterate through all pages to ensure you capture all intervals. The API returns a nextPage token if more data exists.

def fetch_analytics_data(token: str, query_payload: dict) -> list:
    """
    Fetches all pages of analytics data for the given query.
    
    Args:
        token: The OAuth access token.
        query_payload: The JSON payload for the query.
        
    Returns:
        A list of interval data objects.
    """
    url = f"https://api.{GENESYS_CLOUD_REGION}/api/v2/analytics/conversations/details/query"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json"
    }
    
    all_data = []
    next_page_token = None
    
    while True:
        if next_page_token:
            query_payload["pageToken"] = next_page_token
            
        try:
            response = requests.post(url, json=query_payload, headers=headers)
        except requests.exceptions.RequestException as e:
            raise Exception(f"Network error during analytics query: {e}")

        if response.status_code == 429:
            # Rate limit hit. Wait and retry.
            retry_after = int(response.headers.get("Retry-After", 5))
            print(f"Rate limited. Waiting {retry_after} seconds...")
            time.sleep(retry_after)
            continue
            
        if response.status_code not in [200, 201]:
            raise Exception(f"API Error {response.status_code}: {response.text}")

        result = response.json()
        
        # Extract the data array from the response
        if "data" in result and result["data"]:
            all_data.extend(result["data"])
        
        # Check for next page
        next_page_token = result.get("nextPage")
        if not next_page_token:
            break
            
    return all_data

Step 3: Calculate Service Level from Raw Counts

While the API provides a serviceLevel metric, calculating it from raw offered and answered counts ensures you understand the underlying logic and allows for custom SLA definitions (e.g., calculating SLA for different time buckets simultaneously).

Formula:
$$ \text{Service Level %} = \left( \frac{\text{Answered within SLA Target}}{\text{Offered}} \right) \times 100 $$

Note: The Genesys Cloud API serviceLevel metric in the conversation view typically reports the percentage of conversations answered within the queue’s configured service level target. To calculate a custom target (e.g., 30 seconds when the queue is set to 20), you must use the answered metric filtered by duration, or use the serviceLevel metric if it matches your target. For this tutorial, we will demonstrate how to aggregate the standard API-provided serviceLevel metric, but also show how to derive it if you only had offered and answered counts for a specific threshold.

Since the conversation view aggregates data per interval, we will sum the totals across all intervals to get a daily total.

def calculate_aggregate_service_level(data: list, target_seconds: int) -> dict:
    """
    Aggregates interval data to calculate total Service Level.
    
    Note: The 'serviceLevel' metric in the API response is already a percentage 
    for that specific interval. To get a true aggregate percentage for the day, 
    we cannot simply average the percentages. We must sum the numerators (answered) 
    and denominators (offered) first.
    
    However, the API 'serviceLevel' metric is pre-calculated based on the queue's 
    configured SLA. If you want to calculate SLA for a custom threshold, you need 
    the 'answered' count within that threshold. The standard 'conversation' view 
    does not expose 'answered_within_X_seconds' directly as a separate metric 
    unless you use the 'serviceLevel' metric which reflects the queue setting.
    
    For this example, we will calculate the Weighted Average Service Level 
    based on the 'offered' and 'answered' counts if we assume 'answered' means 
    'answered within SLA'. 
    CRITICAL: In Genesys Cloud, the 'answered' metric in the conversation view 
    usually means 'total answered'. The 'serviceLevel' metric is the % answered 
    within the queue's SLA target.
    
    To strictly calculate SLA from raw numbers, we need the count of conversations 
    answered within the target. The API does not provide 'answered_within_target' 
    as a separate raw count in the basic conversation view. 
    Therefore, the most accurate way using this endpoint is to use the provided 
    'serviceLevel' metric if it matches your target, or use the 'analytics:query' 
    with a custom filter if available.
    
    For this tutorial, we will demonstrate the correct mathematical aggregation 
    assuming we have the numerator (answered within SLA) and denominator (offered).
    Since we are using the standard view, we will use the 'serviceLevel' metric 
    provided by Genesys, which is accurate for the queue's configured target.
    """
    
    total_offered = 0
    total_weighted_sl = 0
    
    # We will also calculate a simple average for comparison, though weighted is correct.
    simple_sl_sum = 0
    count_intervals = 0
    
    for interval in data:
        offered = interval.get("offered", 0)
        sl_percentage = interval.get("serviceLevel", 0) # This is a decimal, e.g., 0.85 for 85%
        
        if offered > 0:
            # Add to weighted total
            # SL is a percentage of offered. So, answered_within_sla = offered * sl_percentage
            answered_within_sla = offered * sl_percentage
            total_offered += offered
            total_weighted_sl += answered_within_sla
            
            simple_sl_sum += sl_percentage
            count_intervals += 1
            
    if total_offered == 0:
        return {
            "total_offered": 0,
            "total_answered_within_sla": 0,
            "service_level_percentage": 0.0,
            "message": "No offered conversations found in this period."
        }
        
    # Calculate final weighted percentage
    final_sl_percentage = (total_weighted_sl / total_offered) * 100
    
    return {
        "total_offered": total_offered,
        "total_answered_within_sla": int(total_weighted_sl),
        "service_level_percentage": round(final_sl_percentage, 2),
        "intervals_processed": count_intervals
    }

Step 4: Main Execution Logic

Combine the authentication, query building, fetching, and calculation steps.

import sys
from datetime import datetime, timedelta
import pytz

def main():
    # 1. Setup Time Range (Last 24 Hours)
    now = datetime.now(pytz.UTC)
    start_date = (now - timedelta(hours=24)).isoformat()
    end_date = now.isoformat()
    
    # 2. Configuration
    QUEUE_ID = "your_queue_id_here" # Replace with your Queue ID
    
    if QUEUE_ID == "your_queue_id_here":
        print("Error: Please provide a valid QUEUE_ID in the script.")
        sys.exit(1)

    print(f"Fetching analytics for Queue: {QUEUE_ID}")
    print(f"Period: {start_date} to {end_date}")

    try:
        # 3. Authenticate
        token = get_access_token()
        print("Authentication successful.")
        
        # 4. Build Query
        query_payload = build_query_payload(start_date, end_date, QUEUE_ID)
        
        # 5. Fetch Data
        data = fetch_analytics_data(token, query_payload)
        
        if not data:
            print("No data returned for the specified period and queue.")
            return

        print(f"Retrieved {len(data)} intervals.")
        
        # 6. Calculate Service Level
        result = calculate_aggregate_service_level(data, 20) # 20 seconds target
        
        # 7. Output Results
        print("\n--- Service Level Report ---")
        print(f"Total Offered: {result['total_offered']}")
        print(f"Total Answered within SLA: {result['total_answered_within_sla']}")
        print(f"Service Level Percentage: {result['service_level_percentage']}%")
        print(f"Intervals Processed: {result['intervals_processed']}")
        
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Complete Working Example

import requests
import time
import json
import sys
from datetime import datetime, timedelta
import pytz
from typing import Optional, List, Dict

# Configuration
GENESYS_CLOUD_REGION = "mypurecloud.com" # Change to your region
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
QUEUE_ID = "your_queue_id" # Change to your Queue ID

OAUTH_TOKEN_URL = f"https://api.{GENESYS_CLOUD_REGION}/oauth/token"
ANALYTICS_URL = f"https://api.{GENESYS_CLOUD_REGION}/api/v2/analytics/conversations/details/query"

def get_access_token() -> str:
    """Retrieves an OAuth access token using client credentials."""
    payload = {
        "grant_type": "client_credentials"
    }
    try:
        response = requests.post(
            OAUTH_TOKEN_URL,
            data=payload,
            auth=(CLIENT_ID, CLIENT_SECRET),
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            timeout=10
        )
        response.raise_for_status()
        return response.json()["access_token"]
    except requests.exceptions.HTTPError as e:
        raise Exception(f"Auth failed: {e.response.text}")
    except Exception as e:
        raise Exception(f"Network error: {e}")

def build_query_payload(start_date: str, end_date: str, queue_id: str) -> dict:
    """Constructs the query payload for the analytics API."""
    return {
        "view": "conversation",
        "dateFrom": start_date,
        "dateTo": end_date,
        "groupBy": ["interval"],
        "interval": "PT1H",
        "filter": {
            "type": "and",
            "clauses": [
                {
                    "type": "field",
                    "path": "queue.id",
                    "operator": "eq",
                    "value": queue_id
                },
                {
                    "type": "field",
                    "path": "conversationType",
                    "operator": "eq",
                    "value": "voice"
                }
            ]
        },
        "metrics": [
            "offered",
            "answered",
            "serviceLevel",
            "abandoned"
        ]
    }

def fetch_analytics_data(token: str, query_payload: dict) -> list:
    """Fetches all pages of analytics data."""
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json"
    }
    
    all_data = []
    next_page_token = None
    
    while True:
        if next_page_token:
            query_payload["pageToken"] = next_page_token
            
        try:
            response = requests.post(ANALYTICS_URL, json=query_payload, headers=headers, timeout=30)
        except requests.exceptions.RequestException as e:
            raise Exception(f"Network error: {e}")

        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 5))
            print(f"Rate limited. Waiting {retry_after} seconds...")
            time.sleep(retry_after)
            continue
            
        response.raise_for_status()
        result = response.json()
        
        if "data" in result and result["data"]:
            all_data.extend(result["data"])
            
        next_page_token = result.get("nextPage")
        if not next_page_token:
            break
            
    return all_data

def calculate_aggregate_service_level(data: list) -> dict:
    """Aggregates interval data to calculate total Service Level."""
    total_offered = 0
    total_answered_within_sla = 0.0
    
    for interval in data:
        offered = interval.get("offered", 0)
        sl_percentage = interval.get("serviceLevel", 0) # Decimal, e.g., 0.85
        
        if offered > 0:
            # Calculate the actual count of conversations answered within SLA
            answered_within_sla = offered * sl_percentage
            total_offered += offered
            total_answered_within_sla += answered_within_sla
            
    if total_offered == 0:
        return {
            "total_offered": 0,
            "total_answered_within_sla": 0,
            "service_level_percentage": 0.0
        }
        
    final_sl_percentage = (total_answered_within_sla / total_offered) * 100
    
    return {
        "total_offered": total_offered,
        "total_answered_within_sla": int(total_answered_within_sla),
        "service_level_percentage": round(final_sl_percentage, 2)
    }

def main():
    if CLIENT_ID == "your_client_id" or QUEUE_ID == "your_queue_id":
        print("Error: Please configure CLIENT_ID, CLIENT_SECRET, and QUEUE_ID.")
        sys.exit(1)

    now = datetime.now(pytz.UTC)
    start_date = (now - timedelta(hours=24)).isoformat()
    end_date = now.isoformat()

    print(f"Querying Queue: {QUEUE_ID}")
    print(f"Period: {start_date} to {end_date}")

    try:
        token = get_access_token()
        query_payload = build_query_payload(start_date, end_date, QUEUE_ID)
        data = fetch_analytics_data(token, query_payload)
        
        if not data:
            print("No data found.")
            return

        result = calculate_aggregate_service_level(data)
        
        print("\n--- Results ---")
        print(f"Offered: {result['total_offered']}")
        print(f"Answered within SLA: {result['total_answered_within_sla']}")
        print(f"Service Level: {result['service_level_percentage']}%")
        
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token is invalid, expired, or the client ID/secret is incorrect.
  • Fix: Verify your client credentials in the Genesys Cloud Admin Portal. Ensure the token is being refreshed if it has an expiration time (usually 1 hour).

Error: 403 Forbidden

  • Cause: The OAuth client lacks the analytics:query scope.
  • Fix: Go to Admin > Security > OAuth Clients. Edit your client and add analytics:query to the Scopes list.

Error: 429 Too Many Requests

  • Cause: You have exceeded the rate limit for the Analytics API.
  • Fix: Implement exponential backoff. The code above includes a basic retry mechanism for 429 errors.

Error: Empty Data Response

  • Cause: No conversations match the filter criteria (Queue ID, Date Range, Conversation Type).
  • Fix: Verify the Queue ID is correct. Check that the date range falls within the last 24 months (Analytics API retention limit). Ensure the conversation type (e.g., “voice”) matches your queue’s configuration.

Official References