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
wrapUpCodeis 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
wrapUpCodereturns 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:readanalytics: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:
- The interaction is an Agent interaction (Voice, Email, Chat, Message).
- The agent explicitly selects a wrap-up code from the list of available codes defined in their skill or group configuration.
- The interaction has reached the
wrapupstate.
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
- 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.
- 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.
- Unassigned Callbacks: If a callback is scheduled but never answered, the interaction may remain in a state where no wrap-up was applied.
- 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_IDandGENESYS_CLIENT_SECRETare correct. If running a long script, re-authenticate or use the SDK’s token refresh mechanism. TheApiClient.authenticate()method handles the initial token request.
Error: 403 Forbidden
- What causes it: The OAuth client lacks the
analytics:conversation:readscope. - How to fix it: Go to the Genesys Cloud Admin UI > Platform > OAuth > Edit Client > Scopes. Add
analytics:conversation:readandanalytics: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
tenacityorbackoff.
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
filterByto include only interactions with an agent.
Alternatively, check the"filterBy": [ {"type": "agent", "id": "all"} # This is a conceptual filter; actual syntax depends on view ]agentIdfield in the response. IfagentIdis null, no agent handled the call, sowrapUpCodewill be null.