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

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

What You Will Build

  • You will build a Python script that queries the Genesys Cloud Analytics API for conversation details and correctly identifies why wrapUpCode appears as null.
  • You will use the purecloud-platform-client-v2 Python SDK to fetch raw data and validate the underlying postInteraction data.
  • You will implement logic to distinguish between missing wrap-up codes due to data latency, incorrect filtering, or the absence of a wrap-up phase in the conversation type.

Prerequisites

  • OAuth Client Type: Service Account (Client Credentials Flow) or User-to-User (Authorization Code Flow).
  • Required Scopes:
    • analytics:conversation:read (Required for querying analytics details)
    • interaction:postinteraction:read (Optional but recommended for validating the source truth of the wrap-up code)
  • SDK Version: purecloud-platform-client-v2 >= 163.0.0 (Ensure you are using a recent version to support the latest enum definitions).
  • Language/Runtime: Python 3.8+
  • External Dependencies:
    • purecloud-platform-client-v2
    • python-dotenv (For secure credential management)

Authentication Setup

Genesys Cloud APIs require OAuth 2.0 authentication. The most robust method for server-side scripts is the Client Credentials flow. This flow exchanges a client ID and secret for an access token. The token expires after 3600 seconds (1 hour). You must handle token expiration by requesting a new token when you receive a 401 Unauthorized response.

Install the SDK:

pip install purecloud-platform-client-v2 python-dotenv

Create a .env file in your project root:

GENESYS_CLIENT_ID=your_client_id
GENESYS_CLIENT_SECRET=your_client_secret
GENESYS_REGION=us-east-1

Initialize the client in your script. The SDK handles the initial token request and basic caching, but you must implement retry logic for production workloads.

import os
from purecloud_platform_client_v2 import PlatformClient, Configuration
from dotenv import load_dotenv

load_dotenv()

def create_platform_client() -> PlatformClient:
    """
    Creates and returns a configured PlatformClient instance.
    """
    config = Configuration()
    config.host = f"https://api.{os.getenv('GENESYS_REGION')}.mypurecloud.com"
    config.client_id = os.getenv('GENESYS_CLIENT_ID')
    config.client_secret = os.getenv('GENESYS_CLIENT_SECRET')
    config.scope = [
        "analytics:conversation:read",
        "interaction:postinteraction:read"
    ]
    
    platform_client = PlatformClient(config)
    return platform_client

# Initialize the client
client = create_platform_client()

Implementation

Step 1: Constructing the Analytics Detail Query

The primary endpoint for retrieving conversation details is POST /api/v2/analytics/conversations/details/query. This endpoint accepts a query body that defines the time range, entity filters, and the specific metrics you want to retrieve.

A common mistake is assuming that wrapUpCode is always populated in the wrapUpCode field of the response. In Genesys Cloud, the wrapUpCode is only populated if the conversation explicitly entered the Wrap-Up phase. If a conversation is routed to an agent and the agent ends the interaction without entering a wrap-up state (or if the skill group does not require wrap-up), this field will be null.

Furthermore, analytics data is not real-time. There is a processing latency of approximately 5 to 15 minutes. Querying for a conversation that occurred 2 minutes ago may return no data or incomplete data.

Define the query payload. Note the use of groupBy and filterBy.

from purecloud_platform_client_v2.rest import ApiException
from purecloud_platform_client_v2.models import PostConversationDetailsQueryRequest

def build_detail_query(start_time: str, end_time: str, conversation_id: str) -> dict:
    """
    Builds the request body for the analytics detail query.
    
    Args:
        start_time: ISO 8601 start time (e.g., '2023-10-01T00:00:00.000Z')
        end_time: ISO 8601 end time
        conversation_id: The specific conversation ID to query
    """
    # Define the filter to target a specific conversation
    filter_by = {
        "filters": [
            {
                "dimension": "conversationId",
                "type": "string",
                "value": conversation_id
            }
        ]
    }
    
    # Define the groupBy to ensure we get row-level detail
    group_by = {
        "groupBys": [
            {
                "type": "string",
                "value": "conversationId"
            }
        ]
    }
    
    # Define the metrics to retrieve. 
    # Note: wrapUpCode is part of the 'detail' level data, not a metric.
    # However, we need to specify metrics to get a valid response structure.
    metrics = {
        "metrics": [
            "handlingTime",
            "talkTime",
            "wrapUpTime"
        ]
    }
    
    query_body = {
        "filterBy": filter_by,
        "groupBy": group_by,
        "metrics": metrics,
        "interval": "PT1H" # Hourly interval is standard for detail queries
    }
    
    return query_body

Step 2: Executing the Query and Handling Pagination

The analytics API supports pagination. If you query for a time range that contains many conversations, you must iterate through the pages. For a single conversation ID, pagination is less critical, but it is good practice to handle the nextPageUri if it exists.

def get_conversation_details(conversation_id: str, start_time: str, end_time: str) -> dict:
    """
    Queries the analytics API for conversation details.
    
    Returns:
        dict: The response body containing the conversation details.
    """
    analytics_api = client.analytics_api
    
    query_body = build_detail_query(start_time, end_time, conversation_id)
    
    try:
        # Execute the query
        response = analytics_api.post_analytics_conversations_details_query(
            body=query_body,
            start_time=start_time,
            end_time=end_time
        )
        
        return response.to_dict()
        
    except ApiException as e:
        print(f"Exception when calling AnalyticsApi->post_analytics_conversations_details_query: {e}\n")
        if e.status == 401:
            print("Token expired. Re-authentication required.")
        elif e.status == 429:
            print("Rate limit exceeded. Implement exponential backoff.")
        raise

Step 3: Analyzing the Null Value

When you receive the response, inspect the entities array. Each entity represents a conversation segment. The wrapUpCode field is located in the wrapUpCode attribute of the entity.

However, if wrapUpCode is null, you must determine the root cause. There are three primary reasons:

  1. No Wrap-Up Phase: The conversation type (e.g., Chat, Task, or Voice with specific routing) did not require or allow a wrap-up code. For example, if an agent drops a call immediately, there is no wrap-up.
  2. Data Latency: The conversation is too recent.
  3. Filtering Error: The query did not retrieve the correct segment of the conversation.

To distinguish between these, you must cross-reference the analytics data with the POST /api/v2/interactions/postinteractions/{postInteractionId} endpoint. The postInteraction object contains the definitive wrapUpCode and wrapUpCodeId if one was recorded.

from purecloud_platform_client_v2.models import PostInteraction

def get_post_interaction_details(post_interaction_id: str) -> PostInteraction:
    """
    Retrieves the definitive post-interaction data.
    """
    interactions_api = client.interactions_api
    
    try:
        response = interactions_api.get_interactions_postinteraction(
            post_interaction_id=post_interaction_id
        )
        return response
    except ApiException as e:
        print(f"Exception when getting post-interaction: {e}")
        raise

Step 4: Correlating Analytics and Post-Interaction Data

The analytics API returns a conversationId and often a postInteractionId in the metadata or related objects. If the analytics response does not include the postInteractionId directly in the top-level entity, you may need to query the interactions/conversations endpoint to get the postInteractionId associated with the conversationId.

However, a more direct approach is to check the wrapUpTime metric in the analytics response. If wrapUpTime is 0 or null, it is highly likely that no wrap-up occurred, and thus wrapUpCode will be null. This is not an error; it is the expected behavior.

def analyze_wrap_up_status(analytics_response: dict, conversation_id: str) -> dict:
    """
    Analyzes the analytics response to determine why wrapUpCode is null.
    """
    entities = analytics_response.get('entities', [])
    
    if not entities:
        return {
            "status": "no_data",
            "reason": "No analytics data found. Check time range or data latency."
        }
    
    entity = entities[0]
    metrics = entity.get('metrics', {})
    wrap_up_time = metrics.get('wrapUpTime', {}).get('value', 0)
    wrap_up_code = entity.get('wrapUpCode')
    
    if wrap_up_time == 0 or wrap_up_time is None:
        return {
            "status": "no_wrap_up",
            "reason": "Conversation had no wrap-up phase. wrapUpCode is null by design.",
            "wrapUpTime": wrap_up_time
        }
    
    if wrap_up_code is None:
        return {
            "status": "missing_code",
            "reason": "Wrap-up time exists, but code is null. Check post-interaction data.",
            "wrapUpTime": wrap_up_time
        }
    
    return {
        "status": "success",
        "wrapUpCode": wrap_up_code,
        "wrapUpTime": wrap_up_time
    }

Complete Working Example

This script ties together authentication, analytics querying, and post-interaction validation. It queries for a specific conversation and reports the status of the wrap-up code.

import os
import sys
from datetime import datetime, timedelta
from purecloud_platform_client_v2 import PlatformClient, Configuration
from purecloud_platform_client_v2.rest import ApiException
from dotenv import load_dotenv

load_dotenv()

def create_platform_client() -> PlatformClient:
    config = Configuration()
    config.host = f"https://api.{os.getenv('GENESYS_REGION')}.mypurecloud.com"
    config.client_id = os.getenv('GENESYS_CLIENT_ID')
    config.client_secret = os.getenv('GENESYS_CLIENT_SECRET')
    config.scope = ["analytics:conversation:read", "interaction:postinteraction:read"]
    return PlatformClient(config)

def build_detail_query(conversation_id: str) -> dict:
    return {
        "filterBy": {
            "filters": [
                {"dimension": "conversationId", "type": "string", "value": conversation_id}
            ]
        },
        "groupBy": {
            "groupBys": [
                {"type": "string", "value": "conversationId"}
            ]
        },
        "metrics": ["handlingTime", "talkTime", "wrapUpTime"],
        "interval": "PT1H"
    }

def get_conversation_details(client: PlatformClient, conversation_id: str, start_time: str, end_time: str) -> dict:
    analytics_api = client.analytics_api
    query_body = build_detail_query(conversation_id)
    
    try:
        response = analytics_api.post_analytics_conversations_details_query(
            body=query_body,
            start_time=start_time,
            end_time=end_time
        )
        return response.to_dict()
    except ApiException as e:
        print(f"Analytics API Error: {e.status} - {e.reason}")
        return {}

def get_post_interaction(client: PlatformClient, post_interaction_id: str):
    interactions_api = client.interactions_api
    try:
        return interactions_api.get_interactions_postinteraction(post_interaction_id)
    except ApiException as e:
        print(f"Interactions API Error: {e.status} - {e.reason}")
        return None

def main():
    # Configuration
    CONVERSATION_ID = os.getenv('TARGET_CONVERSATION_ID')
    if not CONVERSATION_ID:
        print("Error: TARGET_CONVERSATION_ID not set in .env file.")
        sys.exit(1)

    # Set time range (last 24 hours to ensure data is processed)
    end_time = datetime.utcnow()
    start_time = end_time - timedelta(hours=24)
    
    start_str = start_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
    end_str = end_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")

    client = create_platform_client()
    
    print(f"Querying analytics for conversation: {CONVERSATION_ID}")
    analytics_response = get_conversation_details(client, CONVERSATION_ID, start_str, end_str)
    
    if not analytics_response:
        print("No analytics data returned.")
        return

    entities = analytics_response.get('entities', [])
    if not entities:
        print("No entities found in analytics response. Check latency or ID.")
        return

    entity = entities[0]
    metrics = entity.get('metrics', {})
    wrap_up_time_val = metrics.get('wrapUpTime', {}).get('value', 0)
    wrap_up_code = entity.get('wrapUpCode')
    
    print(f"Analytics Wrap-Up Time: {wrap_up_time_val}")
    print(f"Analytics Wrap-Up Code: {wrap_up_code}")

    if wrap_up_code is None:
        if wrap_up_time_val == 0:
            print("Result: Null wrap-up code is expected. No wrap-up phase occurred.")
        else:
            print("Result: Null wrap-up code with non-zero time. Investigating post-interaction...")
            
            # Attempt to get post-interaction ID from analytics metadata if available
            # Note: The analytics detail response does not always include postInteractionId directly in the entity.
            # You may need to query the conversation API to get the postInteractionId.
            # For this example, we assume you have the postInteractionId or can derive it.
            # In a real scenario, you would query GET /api/v2/conversations/{conversationId} to get postInteractionId.
            
            conversations_api = client.conversations_api
            try:
                conv_detail = conversations_api.get_conversations_conversation(CONVERSATION_ID)
                post_int_id = conv_detail.to_dict().get('postInteractionId')
                
                if post_int_id:
                    post_int = get_post_interaction(client, post_int_id)
                    if post_int:
                        pi_wrap_up = post_int.to_dict().get('wrapUpCode')
                        print(f"Post-Interaction Wrap-Up Code: {pi_wrap_up}")
                        if pi_wrap_up is None:
                            print("Confirmed: Post-interaction also lacks wrap-up code. Data is consistent.")
                        else:
                            print("Discrepancy: Post-interaction has code, but analytics does not. Report to Genesys Support.")
                    else:
                        print("Could not retrieve post-interaction details.")
                else:
                    print("No postInteractionId found in conversation details.")
            except ApiException as e:
                print(f"Error retrieving conversation details: {e}")

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 401 Unauthorized

Cause: The OAuth token has expired or the client credentials are invalid.
Fix: Ensure your .env file contains the correct GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET. The PlatformClient handles token refresh automatically for the first hour. If you are running a long-lived process, implement a wrapper that catches 401 and re-initializes the client.

Error: 403 Forbidden

Cause: The service account does not have the analytics:conversation:read scope.
Fix: Go to the Genesys Cloud Admin Console, navigate to Manage Apps, edit your app, and ensure the Analytics section has Conversation read access enabled.

Error: Null wrapUpCode with Non-Zero wrapUpTime

Cause: This is a rare edge case where the wrap-up timer started, but the agent did not select a code before ending the interaction, or the system defaulted to a null state.
Fix: Query the POST /api/v2/interactions/postinteractions/{postInteractionId} endpoint. The wrapUpCode field in the post-interaction object is the source of truth. If it is null there, the data is correct. If it is populated there but null in analytics, this is a synchronization bug. Contact Genesys Cloud Support with the conversationId and postInteractionId.

Error: No Data Returned

Cause: Data latency. Analytics data is not available immediately after a conversation ends.
Fix: Wait at least 15 minutes after the conversation end time before querying. Ensure your startTime and endTime parameters in the query cover the actual end time of the conversation.

Official References