Troubleshooting Null Wrap-Up Codes in Genesys Cloud Analytics Detail Queries

Troubleshooting Null Wrap-Up Codes in Genesys Cloud Analytics Detail Queries

What You Will Build

  • You will build a Python script that queries Genesys Cloud Analytics for conversation details and correctly handles cases where wrapUpCode is null.
  • You will use the Genesys Cloud Python SDK (genesyscloud) and the REST API endpoint /api/v2/analytics/conversations/details/query.
  • You will learn why wrapUpCode returns null for specific interaction types and how to filter or post-process these results effectively.

Prerequisites

  • OAuth Client: A Genesys Cloud OAuth client with the following scopes:
    • analytics:conversation:read
    • analytics:conversation:query
  • SDK Version: genesyscloud >= 11.0.0 (Python).
  • Language/Runtime: Python 3.8+.
  • External Dependencies:
    pip install genesyscloud
    

Authentication Setup

Genesys Cloud uses OAuth 2.0 for authentication. For server-to-server integrations and analytics queries, the Client Credentials flow is the standard approach. This flow issues an access token that is valid for 30 minutes (1800 seconds).

The following code demonstrates how to initialize the PureCloudPlatformClientV2 and authenticate. Note that the token is cached internally by the SDK, but you must handle expiration in long-running processes.

import os
from purecloudplatformclientv2 import (
    Configuration,
    ApiClient,
    PureCloudPlatformClientV2,
    AnalyticsApi,
    PostConversationDetailsQueryRequest
)
from purecloudplatformclientv2.rest import ApiException
import time

def get_purecloud_client(client_id: str, client_secret: str, region: str = "us-east-1") -> PureCloudPlatformClientV2:
    """
    Initializes and authenticates the Genesys Cloud client.
    
    Args:
        client_id: OAuth Client ID
        client_secret: OAuth Client Secret
        region: Genesys Cloud region (e.g., us-east-1, eu-west-1)

    Returns:
        Authenticated PureCloudPlatformClientV2 instance
    """
    # Define the base URL based on the region
    base_url = f"https://{region}.mypurecloud.com"
    
    config = Configuration(
        host=base_url,
        oauth_client_id=client_id,
        oauth_client_secret=client_secret
    )
    
    api_client = ApiClient(configuration=config)
    
    try:
        # Authenticate using client credentials
        api_client.authenticate()
        print("Authentication successful.")
        return PureCloudPlatformClientV2(api_client)
    except ApiException as e:
        print(f"Authentication failed: {e.status} - {e.reason}")
        raise

# Usage Example
# CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
# CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")
# purecloud_client = get_purecloud_client(CLIENT_ID, CLIENT_SECRET)

Implementation

Step 1: Constructing the Analytics Detail Query

The core of this tutorial is the POST /api/v2/analytics/conversations/details/query endpoint. This endpoint allows you to retrieve granular data about individual interactions.

A common misconception is that every completed conversation has a wrapUpCode. This is false. The wrapUpCode is only populated when:

  1. The interaction is an Agent interaction (Voice, Email, Chat, Message).
  2. The agent explicitly selects a wrap-up code from the list of available codes defined in their skill or group configuration.
  3. The interaction has reached the wrapup state.

If the interaction is an IVR-only call, a callback that was never answered, or a digital interaction where the agent did not assign a code, the wrapUpCode field in the response will be null.

Building the Request Body

You must define the interval, view, and filterBy parameters. To troubleshoot null values, you often want to query for all conversations in a timeframe and then inspect the result set.

def build_detail_query_request(start_time: str, end_time: str) -> dict:
    """
    Constructs the JSON body for the conversation details query.
    
    Args:
        start_time: ISO 8601 start time (e.g., "2023-10-01T00:00:00Z")
        end_time: ISO 8601 end time (e.g., "2023-10-02T00:00:00Z")

    Returns:
        Dictionary representing the PostConversationDetailsQueryRequest
    """
    query_request = {
        "interval": f"{start_time}/{end_time}",
        "view": "conversation",
        "filterBy": [],
        "groupBy": [],
        "select": [
            "id",
            "type",
            "wrapUpCode",
            "wrapUpCodeName",
            "agentId",
            "agentName",
            "state",
            "startTime",
            "endTime"
        ],
        "size": 200, # Maximum page size is 200
        "pageToken": None
    }
    return query_request

Critical Note on Scopes: Ensure your OAuth token includes analytics:conversation:read. Without this, the API will return a 403 Forbidden error, which can sometimes be mistaken for a data issue if error handling is poor.

Step 2: Executing the Query and Handling Pagination

Analytics queries often return large datasets. You must implement pagination using the pageToken returned in the response header or body (depending on SDK version and endpoint specifics). In the Python SDK, the analytics_api.post_conversations_details_query method returns a response object that contains the data and metadata.

def fetch_all_conversations(purecloud_client: PureCloudPlatformClientV2, query_body: dict) -> list:
    """
    Fetches all conversation details across multiple pages.
    
    Args:
        purecloud_client: Authenticated Genesys Cloud client
        query_body: The query request dictionary

    Returns:
        List of conversation detail objects
    """
    analytics_api = AnalyticsApi(purecloud_client)
    all_conversations = []
    page_token = None
    page_count = 0

    while True:
        # Update the page token in the request body
        query_body["pageToken"] = page_token
        
        try:
            # Execute the API call
            response = analytics_api.post_conversations_details_query(body=query_body)
            
            # Append the results to our list
            if response.entities:
                all_conversations.extend(response.entities)
                print(f"Fetched {len(response.entities)} conversations. Total so far: {len(all_conversations)}")
            
            # Check for next page
            page_token = response.page_token
            page_count += 1
            
            # If no page token is returned, we are on the last page
            if not page_token:
                break
                
            # Optional: Add a small delay to respect rate limits if querying large datasets
            # time.sleep(0.1) 
            
        except ApiException as e:
            if e.status == 429:
                print("Rate limited (429). Retrying in 10 seconds...")
                time.sleep(10)
                continue
            else:
                print(f"API Error {e.status}: {e.reason}")
                raise
        except Exception as e:
            print(f"Unexpected error: {e}")
            break

    return all_conversations

Step 3: Analyzing Null Wrap-Up Codes

Now that you have the data, you must process it to understand why wrapUpCode is null. This step distinguishes between a query error (you asked for the wrong data) and a known limitation (the data does not exist for that interaction type).

Common Reasons for Null wrapUpCode

  1. IVR-Only Interactions: If a caller enters the IVR and hangs up before speaking to an agent, there is no agent to assign a wrap-up code.
  2. Digital Interactions (Chat/Message): Some digital channels allow agents to close chats without selecting a specific wrap-up code, depending on the organization’s configuration.
  3. Unassigned Callbacks: If a callback is scheduled but never answered, the interaction may remain in a state where no wrap-up was applied.
  4. System-Generated Interactions: Certain system-initiated interactions do not support wrap-up codes.

Filtering and Reporting

The following code iterates through the results and categorizes them.

def analyze_wrapup_codes(conversations: list) -> dict:
    """
    Analyzes the retrieved conversations to categorize null wrap-up codes.
    
    Args:
        conversations: List of conversation detail objects

    Returns:
        Dictionary with counts of valid and null wrap-up codes
    """
    stats = {
        "total": len(conversations),
        "with_wrapup": 0,
        "null_wrapup": 0,
        "null_reasons": {
            "ivr_only": 0,
            "digital_no_code": 0,
            "other": 0
        }
    }
    
    null_details = []

    for conv in conversations:
        # Check if wrapUpCode is null
        if conv.wrap_up_code is None:
            stats["null_wrapup"] += 1
            
            # Determine the likely reason
            conv_type = conv.type
            state = conv.state
            agent_id = conv.agent_id
            
            if conv_type == "voice" and agent_id is None:
                # Voice call with no agent assigned -> IVR only
                stats["null_reasons"]["ivr_only"] += 1
            elif conv_type in ["chat", "message", "email"]:
                # Digital interaction without a code
                stats["null_reasons"]["digital_no_code"] += 1
            else:
                # Other cases (e.g., answered but no code selected)
                stats["null_reasons"]["other"] += 1
            
            # Collect details for debugging
            null_details.append({
                "id": conv.id,
                "type": conv_type,
                "state": state,
                "agent_id": agent_id
            })
        else:
            stats["with_wrapup"] += 1
            
    stats["null_details_sample"] = null_details[:5] # Keep first 5 for inspection
    return stats

Complete Working Example

Below is the complete, copy-pasteable Python script. You must replace YOUR_CLIENT_ID and YOUR_CLIENT_SECRET with your actual OAuth credentials.

import os
import time
from datetime import datetime, timedelta
from purecloudplatformclientv2 import (
    Configuration,
    ApiClient,
    PureCloudPlatformClientV2,
    AnalyticsApi
)
from purecloudplatformclientv2.rest import ApiException

def get_purecloud_client(client_id: str, client_secret: str, region: str = "us-east-1") -> PureCloudPlatformClientV2:
    """Initializes and authenticates the Genesys Cloud client."""
    base_url = f"https://{region}.mypurecloud.com"
    config = Configuration(
        host=base_url,
        oauth_client_id=client_id,
        oauth_client_secret=client_secret
    )
    api_client = ApiClient(configuration=config)
    try:
        api_client.authenticate()
        return PureCloudPlatformClientV2(api_client)
    except ApiException as e:
        print(f"Authentication failed: {e.status} - {e.reason}")
        raise

def build_detail_query_request(start_time: str, end_time: str) -> dict:
    """Constructs the JSON body for the conversation details query."""
    return {
        "interval": f"{start_time}/{end_time}",
        "view": "conversation",
        "filterBy": [],
        "groupBy": [],
        "select": [
            "id",
            "type",
            "wrapUpCode",
            "wrapUpCodeName",
            "agentId",
            "agentName",
            "state",
            "startTime",
            "endTime"
        ],
        "size": 200,
        "pageToken": None
    }

def fetch_all_conversations(purecloud_client: PureCloudPlatformClientV2, query_body: dict) -> list:
    """Fetches all conversation details across multiple pages."""
    analytics_api = AnalyticsApi(purecloud_client)
    all_conversations = []
    page_token = None
    page_count = 0

    while True:
        query_body["pageToken"] = page_token
        try:
            response = analytics_api.post_conversations_details_query(body=query_body)
            if response.entities:
                all_conversations.extend(response.entities)
            page_token = response.page_token
            page_count += 1
            if not page_token:
                break
        except ApiException as e:
            if e.status == 429:
                print("Rate limited (429). Retrying in 10 seconds...")
                time.sleep(10)
                continue
            else:
                raise
        except Exception as e:
            print(f"Unexpected error: {e}")
            break
    return all_conversations

def analyze_wrapup_codes(conversations: list) -> dict:
    """Analyzes the retrieved conversations to categorize null wrap-up codes."""
    stats = {
        "total": len(conversations),
        "with_wrapup": 0,
        "null_wrapup": 0,
        "null_reasons": {
            "ivr_only": 0,
            "digital_no_code": 0,
            "other": 0
        }
    }
    
    for conv in conversations:
        if conv.wrap_up_code is None:
            stats["null_wrapup"] += 1
            conv_type = conv.type
            agent_id = conv.agent_id
            
            if conv_type == "voice" and agent_id is None:
                stats["null_reasons"]["ivr_only"] += 1
            elif conv_type in ["chat", "message", "email"]:
                stats["null_reasons"]["digital_no_code"] += 1
            else:
                stats["null_reasons"]["other"] += 1
        else:
            stats["with_wrapup"] += 1
            
    return stats

def main():
    # Configuration
    CLIENT_ID = os.getenv("GENESYS_CLIENT_ID", "YOUR_CLIENT_ID")
    CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET", "YOUR_CLIENT_SECRET")
    REGION = "us-east-1"
    
    # Time Range: Last 24 hours
    end_time = datetime.utcnow()
    start_time = end_time - timedelta(hours=24)
    
    start_iso = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
    end_iso = end_time.strftime("%Y-%m-%dT%H:%M:%SZ")

    print(f"Querying conversations from {start_iso} to {end_iso}")

    try:
        # 1. Authenticate
        purecloud_client = get_purecloud_client(CLIENT_ID, CLIENT_SECRET, REGION)
        
        # 2. Build Query
        query_body = build_detail_query_request(start_iso, end_iso)
        
        # 3. Fetch Data
        conversations = fetch_all_conversations(purecloud_client, query_body)
        print(f"Total conversations fetched: {len(conversations)}")
        
        # 4. Analyze
        stats = analyze_wrapup_codes(conversations)
        
        # 5. Report
        print("\n--- Wrap-Up Code Analysis ---")
        print(f"Total Interactions: {stats['total']}")
        print(f"Interactions WITH Wrap-Up Code: {stats['with_wrapup']}")
        print(f"Interactions WITHOUT Wrap-Up Code: {stats['null_wrapup']}")
        print("\nReasons for Null Wrap-Up:")
        print(f"  IVR Only (Voice, No Agent): {stats['null_reasons']['ivr_only']}")
        print(f"  Digital (No Code Selected): {stats['null_reasons']['digital_no_code']}")
        print(f"  Other: {stats['null_reasons']['other']}")

    except Exception as e:
        print(f"Script failed: {e}")

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 401 Unauthorized

  • What causes it: The OAuth token has expired or the client credentials are incorrect.
  • How to fix it: Ensure GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET are correct. If running a long script, re-authenticate or use the SDK’s token refresh mechanism. The ApiClient.authenticate() method handles the initial token request.

Error: 403 Forbidden

  • What causes it: The OAuth client lacks the analytics:conversation:read scope.
  • How to fix it: Go to the Genesys Cloud Admin UI > Platform > OAuth > Edit Client > Scopes. Add analytics:conversation:read and analytics:conversation:query. Save the changes. The new scopes apply to new tokens only.

Error: 429 Too Many Requests

  • What causes it: You are hitting the Genesys Cloud API rate limits. Analytics endpoints have specific rate limits per tenant.
  • How to fix it: Implement exponential backoff. The code example above includes a simple retry for 429s. For production, use a library like tenacity or backoff.

Error: wrapUpCode is always null

  • What causes it: You are querying an interaction type that does not support wrap-up codes (e.g., IVR-only voice calls) or the time interval contains no agent-handled interactions.
  • How to fix it: Filter your query by filterBy to include only interactions with an agent.
    "filterBy": [
        {"type": "agent", "id": "all"} # This is a conceptual filter; actual syntax depends on view
    ]
    
    Alternatively, check the agentId field in the response. If agentId is null, no agent handled the call, so wrapUpCode will be null.

Official References