Choose the Right Genesys Cloud API: Real-Time Conversations vs. Analytics Data
What You Will Build
- You will build a Python application that retrieves active conversation details using the real-time API and historical conversation metrics using the analytics API.
- This tutorial uses the Genesys Cloud CX REST API v2 and the official
genesys-cloud-pythonSDK. - The implementation is written in Python 3.9+ using the
requestslibrary for raw HTTP examples and the SDK for structured data access.
Prerequisites
Before writing code, verify your environment meets these requirements:
- OAuth Client ID and Secret: You must have a Genesys Cloud application created in the Admin console.
- Required Scopes:
- For
/api/v2/conversations:conversation:view,conversation:participant:write(if modifying state). - For
/api/v2/analytics/conversations/details/query:analytics:conversation:view.
- For
- Python Environment: Python 3.9 or later.
- Dependencies:
pip install requests genesys-cloud-python python-dotenv - Environment Variables: Create a
.envfile with your credentials:GENESYS_CLOUD_REGION=us-east-1 GENESYS_CLOUD_CLIENT_ID=your_client_id GENESYS_CLOUD_CLIENT_SECRET=your_client_secret
Authentication Setup
Genesys Cloud APIs require OAuth 2.0 client credentials flow. The token is short-lived (typically 1 hour), so production code must handle refresh or re-authentication. The following helper class manages this lifecycle.
import os
import time
import requests
from dotenv import load_dotenv
load_dotenv()
class GenesysAuth:
def __init__(self, region: str = "us-east-1"):
self.region = region
self.client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
self.client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
self.auth_base_url = f"https://api.{region}.mypurecloud.com"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
# Check if we have a valid cached token
if self.access_token and time.time() < self.token_expiry:
return self.access_token
# Request new token
auth_url = f"{self.auth_base_url}/oauth/token"
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "conversation:view analytics:conversation:view"
}
try:
response = requests.post(auth_url, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
# Set expiry to slightly before actual expiry to avoid race conditions
self.token_expiry = time.time() + (token_data["expires_in"] - 60)
return self.access_token
except requests.exceptions.HTTPError as e:
print(f"Authentication failed: {e.response.status_code} - {e.response.text}")
raise
# Initialize auth
auth = GenesysAuth()
This setup ensures that every subsequent API call uses a valid token without requiring manual token management in your business logic.
Implementation
The core distinction between the two endpoints lies in state versus history.
/api/v2/conversationsis operational. It returns the current state of active interactions (voice, chat, message, video). It is used for real-time actions: transferring, recording, adding participants, or updating presence./api/v2/analytics/conversations/details/queryis historical. It returns aggregated or detailed records of completed interactions. It is used for reporting, billing, quality assurance, and post-call analysis.
Step 1: Retrieving Real-Time Conversations
When you need to know what is happening right now, you query the conversations API. This endpoint supports filtering by user, queue, or conversation type.
Endpoint: GET /api/v2/conversations
Scope: conversation:view
import requests
from typing import List, Dict, Any
def get_active_conversations(auth: GenesysAuth, limit: int = 20) -> List[Dict[str, Any]]:
"""
Retrieves a list of currently active conversations.
"""
base_url = f"https://api.{auth.region}.mypurecloud.com"
endpoint = "/api/v2/conversations"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
params = {
"limit": limit,
"expand": "participants" # Include participant details in the response
}
try:
response = requests.get(f"{base_url}{endpoint}", headers=headers, params=params)
response.raise_for_status()
return response.json()["entities"]
except requests.exceptions.HTTPError as e:
if response.status_code == 429:
print("Rate limited. Wait and retry.")
time.sleep(1) # Simple backoff
return get_active_conversations(auth, limit)
else:
print(f"Error fetching conversations: {e.response.status_code}")
raise
# Usage
active_convs = get_active_conversations(auth)
if active_convs:
print(f"Found {len(active_convs)} active conversations.")
for conv in active_convs:
print(f"ID: {conv['id']}, Type: {conv['type']}, State: {conv['state']}")
else:
print("No active conversations found.")
Key Response Fields:
id: The unique identifier for the conversation.type:voice,chat,message,email,sms, orvideo.state:active,closed,ended, orunknown. For real-time API,activeis the primary state of interest.participants: An array of objects containinguserId,name, andstate(e.g.,ringing,talking,listening).
Why use this endpoint?
Use this when building a supervisor dashboard that shows who is talking now, or an IVR script that needs to check if an agent is currently on a call before routing.
Step 2: Querying Historical Analytics Data
When a conversation ends, its data moves to the analytics store. You cannot retrieve a closed conversation via /api/v2/conversations. You must use the analytics query API.
Endpoint: POST /api/v2/analytics/conversations/details/query
Scope: analytics:conversation:view
This endpoint uses a complex JSON body to define the query. It supports date ranges, filters, and sorting.
import json
from datetime import datetime, timedelta
def get_historical_conversations(auth: GenesysAuth, days_back: int = 7) -> Dict[str, Any]:
"""
Queries completed conversations from the last N days.
"""
base_url = f"https://api.{auth.region}.mypurecloud.com"
endpoint = "/api/v2/anversations/details/query" # Note: Correct path is /analytics/conversations/details/query
# Fix endpoint path
endpoint = "/api/v2/analytics/conversations/details/query"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
# Define the time range
end_time = datetime.utcnow().isoformat() + "Z"
start_time = (datetime.utcnow() - timedelta(days=days_back)).isoformat() + "Z"
# Construct the query body
query_body = {
"dateFrom": start_time,
"dateTo": end_time,
"interval": "PT1H", # Aggregate by hour (optional, but recommended for large datasets)
"view": "conversationDetail", # Specific view for detailed records
"select": [
"conversationId",
"conversationType",
"start",
"end",
"duration",
"wrapupCode",
"userId",
"userName"
],
"where": [
{
"path": "conversationType",
"constraint": "voice" # Filter for voice calls only
},
{
"path": "state",
"constraint": "closed" # Only closed conversations
}
],
"size": 100 # Max records per page
}
try:
response = requests.post(
f"{base_url}{endpoint}",
headers=headers,
json=query_body
)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(f"Analytics query failed: {e.response.status_code}")
print(e.response.text)
raise
# Usage
analytics_data = get_historical_conversations(auth, days_back=1)
if "entities" in analytics_data:
print(f"Retrieved {len(analytics_data['entities'])} historical records.")
for entity in analytics_data["entities"][:5]: # Show first 5
print(f"Call ID: {entity.get('conversationId')}, Duration: {entity.get('duration')}")
else:
print("No historical data found in the specified range.")
Key Differences in Request:
- Method:
POSTinstead ofGET. The query parameters are too complex for a URL string. - Body: Uses a structured JSON object with
select,where, andviewclauses. - Data Scope: Returns only
closedorendedconversations. Active conversations will not appear here until they terminate.
Why use this endpoint?
Use this for generating end-of-day reports, calculating Average Handle Time (AHT), or exporting call logs for compliance auditing.
Step 3: Handling Pagination and Large Datasets
The analytics API returns paginated results. If you request more data than the size limit (max 1000 for details query), you must handle the nextPageToken.
def get_all_historical_conversations(auth: GenesysAuth, days_back: int = 1) -> List[Dict]:
"""
Fetches all pages of historical conversations.
"""
base_url = f"https://api.{auth.region}.mypurecloud.com"
endpoint = "/api/v2/analytics/conversations/details/query"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
end_time = datetime.utcnow().isoformat() + "Z"
start_time = (datetime.utcnow() - timedelta(days=days_back)).isoformat() + "Z"
base_query = {
"dateFrom": start_time,
"dateTo": end_time,
"view": "conversationDetail",
"select": ["conversationId", "duration"],
"where": [{"path": "state", "constraint": "closed"}],
"size": 100
}
all_records = []
page_token = None
while True:
query = base_query.copy()
if page_token:
query["pageToken"] = page_token
response = requests.post(f"{base_url}{endpoint}", headers=headers, json=query)
response.raise_for_status()
data = response.json()
if "entities" in data:
all_records.extend(data["entities"])
# Check for next page
if data.get("nextPageToken"):
page_token = data["nextPageToken"]
# Add a small delay to respect rate limits
time.sleep(0.5)
else:
break
return all_records
# Usage
all_calls = get_all_historical_conversations(auth)
print(f"Total historical records fetched: {len(all_calls)}")
This pattern ensures you capture the entire dataset without manual intervention.
Complete Working Example
Combine the authentication and query logic into a single script that demonstrates both real-time and historical access.
import os
import time
import requests
from datetime import datetime, timedelta
from typing import List, Dict, Any
from dotenv import load_dotenv
load_dotenv()
class GenesysAPI:
def __init__(self, region: str = "us-east-1"):
self.region = region
self.client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
self.client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
self.auth_base_url = f"https://api.{region}.mypurecloud.com"
self.base_url = f"https://api.{region}.mypurecloud.com"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry:
return self.access_token
auth_url = f"{self.auth_base_url}/oauth/token"
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "conversation:view analytics:conversation:view"
}
try:
response = requests.post(auth_url, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expiry = time.time() + (token_data["expires_in"] - 60)
return self.access_token
except requests.exceptions.HTTPError as e:
print(f"Auth Failed: {e.response.text}")
raise
def get_active_conversations(self) -> List[Dict[str, Any]]:
headers = {"Authorization": f"Bearer {self.get_token()}"}
params = {"limit": 5, "expand": "participants"}
response = requests.get(
f"{self.base_url}/api/v2/conversations",
headers=headers,
params=params
)
response.raise_for_status()
return response.json().get("entities", [])
def get_historical_conversations(self, days_back: int = 1) -> List[Dict]:
headers = {"Authorization": f"Bearer {self.get_token()}", "Content-Type": "application/json"}
end_time = datetime.utcnow().isoformat() + "Z"
start_time = (datetime.utcnow() - timedelta(days=days_back)).isoformat() + "Z"
query_body = {
"dateFrom": start_time,
"dateTo": end_time,
"view": "conversationDetail",
"select": ["conversationId", "conversationType", "duration"],
"where": [{"path": "state", "constraint": "closed"}],
"size": 10
}
response = requests.post(
f"{self.base_url}/api/v2/analytics/conversations/details/query",
headers=headers,
json=query_body
)
response.raise_for_status()
return response.json().get("entities", [])
if __name__ == "__main__":
api = GenesysAPI()
print("--- Real-Time Conversations ---")
try:
active = api.get_active_conversations()
if active:
for conv in active:
print(f"Active: {conv['id']} ({conv['type']})")
else:
print("No active conversations.")
except Exception as e:
print(f"Error fetching active: {e}")
print("\n--- Historical Conversations (Last 1 Day) ---")
try:
history = api.get_historical_conversations(days_back=1)
if history:
for rec in history:
print(f"History: {rec.get('conversationId')} ({rec.get('conversationType')})")
else:
print("No historical conversations found.")
except Exception as e:
print(f"Error fetching history: {e}")
Common Errors & Debugging
Error: 403 Forbidden
Cause: The OAuth token lacks the required scope.
Fix: Ensure your client application in Genesys Cloud Admin has the conversation:view scope for real-time APIs and analytics:conversation:view for analytics APIs. Regenerate the token after updating scopes.
Error: 429 Too Many Requests
Cause: You exceeded the API rate limit. The conversations API has a lower rate limit than analytics.
Fix: Implement exponential backoff. Do not retry immediately. Wait 1 second, then 2, then 4, etc.
def retry_with_backoff(func, *args, max_retries=3, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
wait_time = 2 ** attempt
print(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time)
else:
raise
Error: Empty Entities in Analytics Query
Cause: The date range is in the future, or the state filter excludes all records.
Fix: Verify dateFrom and dateTo are in ISO 8601 format with Z suffix. Ensure state is set to closed for historical data. If you query for active state in analytics, you will get no results because active conversations are not in the analytics store yet.
Error: Conversation ID Not Found in Analytics
Cause: The conversation is still active or recently closed. Analytics data is not real-time. There is typically a latency of 1-5 minutes before a closed conversation appears in the analytics API.
Fix: If you need immediate data after a call ends, use the Real-Time API until the state changes to closed, then switch to Analytics for long-term storage. Do not poll analytics for recent calls.