Debugging 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 interprets
wrapUpCodedata. - You will use the
GET /api/v2/analytics/conversations/details/queryendpoint to retrieve granular conversation metrics. - You will use Python 3.10+ with the
requestslibrary to handle authentication, pagination, and data parsing.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth client with the
analytics:conversation:readscope. - SDK/API Version: Genesys Cloud REST API v2.
- Language/Runtime: Python 3.10 or higher.
- Dependencies:
requests(pip install requests).
Authentication Setup
Genesys Cloud uses OAuth 2.0 for API authentication. For server-to-server integrations, the Client Credentials flow is the standard approach. You must cache the access token and handle expiration. The following function retrieves a valid token.
import requests
import time
import os
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, env_url: str):
self.client_id = client_id
self.client_secret = client_secret
self.env_url = env_url
self.token_url = f"{env_url}/oauth/token"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
"""
Retrieves an OAuth access token if one is not cached or has expired.
"""
# Check if cached token is still valid (buffer 60 seconds)
if self.access_token and time.time() < self.token_expiry - 60:
return self.access_token
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
response = requests.post(self.token_url, headers=headers, data=data)
if response.status_code != 200:
raise Exception(f"Authentication failed: {response.status_code} - {response.text}")
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expiry = time.time() + token_data["expires_in"]
return self.access_token
Implementation
Step 1: Constructing the Analytics Detail Query
The core of this issue lies in how you construct the query body for the GET /api/v2/analytics/conversations/details/query endpoint. Many developers assume that requesting wrapupcode in the metrics array automatically populates the wrapUpCode object in the response. This is incorrect.
To retrieve the specific wrapUpCode ID and name, you must include the wrapupcode metric in your request. However, the response structure depends heavily on whether the conversation actually had a wrap-up code applied by an agent.
def build_query_payload(start_date: str, end_date: str) -> dict:
"""
Constructs the query payload for conversation details.
Args:
start_date: ISO 8601 start date (e.g., "2023-10-01T00:00:00.000Z")
end_date: ISO 8601 end date (e.g., "2023-10-02T00:00:00.000Z")
Returns:
dict: The JSON body for the analytics query.
"""
payload = {
"interval": "PT1H", # Hourly intervals
"dateFrom": start_date,
"dateTo": end_date,
"view": "default",
"groupBy": ["wrapupcode"], # Grouping by wrapupcode ensures it is processed
"metrics": [
"conversationcount",
"wrapupcode" # Critical: Must explicitly request this metric
],
"filters": {
"types": [
"voice" # Wrap-up codes are primarily relevant for Voice and Task
]
}
}
return payload
Why this matters: If you omit "wrapupcode" from the metrics array, the API may return minimal metadata. If you omit it from groupBy, the engine might aggregate data differently, potentially obscuring specific code assignments in summary views, though detail queries usually respect the metrics request more strictly.
Step 2: Executing the Query and Handling Pagination
The Analytics API uses cursor-based pagination. You must follow the nextPageCursor to retrieve all results. This step demonstrates how to send the request and handle the initial response.
def fetch_conversation_details(auth: GenesysAuth, env_url: str, payload: dict) -> list:
"""
Fetches conversation details using the analytics API.
Args:
auth: GenesysAuth instance
env_url: Base URL for the Genesys Cloud environment
payload: The query payload
Returns:
list: A flat list of all conversation detail records.
"""
api_url = f"{env_url}/api/v2/analytics/conversations/details/query"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
all_records = []
while True:
try:
response = requests.post(api_url, headers=headers, json=payload)
if response.status_code == 429:
# Handle Rate Limiting
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 != 200:
raise Exception(f"API Error: {response.status_code} - {response.text}")
data = response.json()
# Extract records
if "records" in data:
all_records.extend(data["records"])
# Check for pagination
if "nextPageCursor" in data and data["nextPageCursor"]:
payload["pageCursor"] = data["nextPageCursor"]
continue
else:
break
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
break
return all_records
Step 3: Processing Results and Diagnosing Null Values
This is the critical section. When you receive the records, you must inspect the metrics object within each record. The wrapUpCode is not a top-level field; it is nested within the metrics object corresponding to the metric name wrapupcode.
There are three reasons you will see null or missing data:
- No Wrap-Up Code Applied: The agent ended the conversation without selecting a code (if optional).
- System-Generated End: The conversation ended via a system event (e.g., timeout, disconnect) before an agent could apply a code.
- Query Misinterpretation: You are looking at the wrong metric key.
def analyze_wrapup_codes(records: list) -> dict:
"""
Analyzes records to extract wrap-up code information.
Args:
records: List of conversation detail records from the API.
Returns:
dict: A summary of wrap-up code occurrences and null counts.
"""
wrapup_stats = {
"total_records": len(records),
"codes_applied": 0,
"codes_null": 0,
"code_distribution": {}
}
for record in records:
# The metrics object is a dictionary where keys are metric names
metrics = record.get("metrics", {})
# Check specifically for the 'wrapupcode' metric
wrapup_metric = metrics.get("wrapupcode")
if wrapup_metric is None:
wrapup_stats["codes_null"] += 1
continue
# The value is an object containing 'id' and 'name'
# Note: In some legacy views, it might just be the ID, but standard v2 returns object
if isinstance(wrapup_metric, dict):
code_id = wrapup_metric.get("id")
code_name = wrapup_metric.get("name")
elif isinstance(wrapup_metric, str):
# Fallback for simpler metric returns
code_id = wrapup_metric
code_name = "Unknown"
else:
code_id = str(wrapup_metric)
code_name = "Unknown"
if code_id is None or code_id == "":
wrapup_stats["codes_null"] += 1
else:
wrapup_stats["codes_applied"] += 1
key = f"{code_id} - {code_name}"
wrapup_stats["code_distribution"][key] = wrapup_stats["code_distribution"].get(key, 0) + 1
return wrapup_stats
Complete Working Example
This script combines authentication, querying, and analysis into a single runnable module. Replace the placeholder credentials with your actual OAuth client details.
import requests
import time
import os
import json
from datetime import datetime, timedelta
# Configuration
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
ENV_URL = "https://api.mypurecloud.com" # Change to your specific environment (e.g., usw2, euw1)
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, env_url: str):
self.client_id = client_id
self.client_secret = client_secret
self.env_url = env_url
self.token_url = f"{env_url}/oauth/token"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry - 60:
return self.access_token
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
response = requests.post(self.token_url, headers=headers, data=data)
if response.status_code != 200:
raise Exception(f"Authentication failed: {response.status_code} - {response.text}")
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expiry = time.time() + token_data["expires_in"]
return self.access_token
def build_query_payload() -> dict:
# Query the last 24 hours
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=1)
return {
"interval": "PT1H",
"dateFrom": start_date.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
"dateTo": end_date.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
"view": "default",
"groupBy": ["wrapupcode"],
"metrics": [
"conversationcount",
"wrapupcode"
],
"filters": {
"types": ["voice"]
}
}
def fetch_conversation_details(auth: GenesysAuth, env_url: str, payload: dict) -> list:
api_url = f"{env_url}/api/v2/analytics/conversations/details/query"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
all_records = []
while True:
try:
response = requests.post(api_url, headers=headers, json=payload)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
time.sleep(retry_after)
continue
if response.status_code != 200:
raise Exception(f"API Error: {response.status_code} - {response.text}")
data = response.json()
if "records" in data:
all_records.extend(data["records"])
if "nextPageCursor" in data and data["nextPageCursor"]:
payload["pageCursor"] = data["nextPageCursor"]
continue
else:
break
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
break
return all_records
def analyze_wrapup_codes(records: list) -> dict:
wrapup_stats = {
"total_records": len(records),
"codes_applied": 0,
"codes_null": 0,
"code_distribution": {}
}
for record in records:
metrics = record.get("metrics", {})
wrapup_metric = metrics.get("wrapupcode")
if wrapup_metric is None:
wrapup_stats["codes_null"] += 1
continue
if isinstance(wrapup_metric, dict):
code_id = wrapup_metric.get("id")
code_name = wrapup_metric.get("name")
elif isinstance(wrapup_metric, str):
code_id = wrapup_metric
code_name = "Unknown"
else:
code_id = str(wrapup_metric)
code_name = "Unknown"
if code_id is None or code_id == "":
wrapup_stats["codes_null"] += 1
else:
wrapup_stats["codes_applied"] += 1
key = f"{code_id} - {code_name}"
wrapup_stats["code_distribution"][key] = wrapup_stats["code_distribution"].get(key, 0) + 1
return wrapup_stats
if __name__ == "__main__":
print("Initializing Genesys Cloud Analytics Query...")
auth = GenesysAuth(CLIENT_ID, CLIENT_SECRET, ENV_URL)
try:
payload = build_query_payload()
print(f"Querying data from {payload['dateFrom']} to {payload['dateTo']}")
records = fetch_conversation_details(auth, ENV_URL, payload)
print(f"Retrieved {len(records)} conversation records.")
if not records:
print("No records found. Check date range and filters.")
else:
stats = analyze_wrapup_codes(records)
print("\n--- Wrap-Up Code Analysis ---")
print(f"Total Records: {stats['total_records']}")
print(f"Codes Applied: {stats['codes_applied']}")
print(f"Null/Empty Codes: {stats['codes_null']}")
if stats['code_distribution']:
print("\nCode Distribution:")
for code, count in stats['code_distribution'].items():
print(f" {code}: {count}")
else:
print("\nNo specific codes found. All records may be null or system-generated.")
except Exception as e:
print(f"Error: {e}")
Common Errors & Debugging
Error: 403 Forbidden
What causes it: The OAuth client does not have the analytics:conversation:read scope.
How to fix it:
- Go to the Genesys Cloud Admin console.
- Navigate to Organization > Clients.
- Select your OAuth client.
- Edit the scopes and add
analytics:conversation:read. - Save and generate a new token.
Error: Metric ‘wrapupcode’ returns null for all records
What causes it:
- No Codes Defined: Your Genesys Cloud instance has no Wrap-Up Codes defined in the IVR/Queue settings, or they are not assigned to the queues included in your query.
- Agents Not Using Codes: Agents are ending conversations without selecting a code (if the configuration allows optional codes).
- Wrong Conversation Type: You are querying
chatoremailconversations. Wrap-up codes are primarily avoiceandtaskfeature. Ensure your filter includes"types": ["voice"].
How to fix it:
- Verify Wrap-Up Codes exist in Admin > Routing > Wrap-up Codes.
- Check the Queue configuration to ensure Wrap-Up Codes are enabled and required.
- Modify the query filter to explicitly include
voicetypes.
Error: 429 Too Many Requests
What causes it: You are sending requests faster than the API allows. Analytics queries are heavy on the database.
How to fix it:
- Implement exponential backoff.
- Reduce the frequency of queries.
- Use the
Retry-Afterheader provided in the 429 response. The code example above handles this automatically.
Error: Unexpected Null in Nested Object
What causes it: You are accessing record["wrapUpCode"] directly instead of record["metrics"]["wrapupcode"].
How to fix it:
The Analytics Detail API returns metrics in a flat dictionary under the metrics key. Always access via record.get("metrics", {}).get("wrapupcode").