Query CSAT Survey Responses via Genesys Cloud Quality API
What You Will Build
- A Python script that queries the Genesys Cloud Quality API to retrieve CSAT survey responses for specific conversation IDs.
- This solution uses the
GET /api/v2/quality/conversations/evaluationsendpoint with filtering logic applied to the response payload. - The tutorial covers Python implementation using the official
genesys-cloud-purecloud-platform-clientSDK.
Prerequisites
- OAuth Client Type: Service Account or Client Credentials Flow.
- Required Scopes:
quality:evaluation:read(Required to access evaluation data)analytics:conversation:read(Optional, if you need to cross-reference conversation details)user:read(Optional, to resolve user IDs to names)
- SDK Version:
genesys-cloud-purecloud-platform-clientv120.0.0 or later. - Language/Runtime: Python 3.8+
- External Dependencies:
genesys-cloud-purecloud-platform-clientpydantic(usually included with SDK)
Authentication Setup
The Genesys Cloud Python SDK handles OAuth token acquisition and refresh internally. You must initialize the ApiClient with your environment URL, client ID, and client secret.
from genesyscloud.configuration import Configuration
from genesyscloud.api_client import ApiClient
import os
def get_api_client():
"""
Initializes and returns a configured ApiClient instance.
Uses environment variables for security.
"""
# Configuration object holds the OAuth settings and base URL
configuration = Configuration(
host="https://api.mypurecloud.com", # Replace with your environment URL
client_id=os.getenv("GENESYS_CLIENT_ID"),
client_secret=os.getenv("GENESYS_CLIENT_SECRET")
)
# The ApiClient manages the OAuth token lifecycle
api_client = ApiClient(configuration=configuration)
return api_client
Note on Scopes: Ensure your OAuth client in the Genesys Cloud Admin Console has the quality:evaluation:read scope enabled. If you receive a 403 Forbidden error, verify this scope first.
Implementation
Step 1: Define the Query Parameters
The Quality API does not have a dedicated endpoint for “CSAT responses only.” Instead, CSAT responses are stored as evaluations of type survey. You must query the /api/v2/quality/conversations/evaluations endpoint and filter the results.
To tie CSAT to specific interactions, you pass the conversationIds parameter.
from genesyscloud.quality.api.evaluations_api import EvaluationsApi
from datetime import datetime, timezone
def build_query_params(conversation_ids: list[str], start_date: datetime, end_date: datetime) -> dict:
"""
Constructs the query parameters for the evaluations API.
Args:
conversation_ids: List of conversation IDs to filter by.
start_date: Start of the date range (ISO 8601).
end_date: End of the date range (ISO 8601).
Returns:
Dictionary of query parameters.
"""
# The API expects ISO 8601 format with timezone
start_str = start_date.isoformat()
end_str = end_date.isoformat()
# Join conversation IDs with commas for the API parameter
conv_id_str = ",".join(conversation_ids)
params = {
"conversationIds": conv_id_str,
"fromDate": start_str,
"toDate": end_str,
"pageSize": 100, # Max page size is 100 for this endpoint
"page": 1
}
return params
Step 2: Execute the Query with Pagination
The Quality Evaluations API supports pagination. You must handle the nextPage token to ensure you retrieve all evaluations associated with the provided conversation IDs, especially if a single conversation has multiple evaluation types (e.g., a QA evaluation and a CSAT survey).
from genesyscloud.rest import ApiException
def fetch_evaluations(api_client: ApiClient, params: dict) -> list:
"""
Fetches all evaluations matching the query parameters, handling pagination.
Args:
api_client: The initialized ApiClient.
params: Dictionary containing query parameters.
Returns:
List of Evaluation entities.
"""
evaluations_api = EvaluationsApi(api_client)
all_evaluations = []
page = 1
max_pages = 10 # Safety break to prevent infinite loops
while page <= max_pages:
try:
# Update page number in params
params["page"] = page
# Call the API
response = evaluations_api.post_quality_conversations_evaluations(
body=None, # Body is not used for filtering in GET-like POST
_from_date=params.get("fromDate"),
_to_date=params.get("toDate"),
conversation_ids=params.get("conversationIds"),
page_size=params.get("pageSize", 100),
page=page
)
if response.entities:
all_evaluations.extend(response.entities)
# Check if there are more pages
if response.next_page is None:
break
page += 1
except ApiException as e:
if e.status == 429:
print(f"Rate limited. Waiting...")
import time
time.sleep(10)
continue
else:
raise e
return all_evaluations
Important: The endpoint post_quality_conversations_evaluations is used for querying with complex filters, even though it is a POST verb. This is standard for Genesys Cloud Analytics and Quality query endpoints.
Step 3: Filter for CSAT and Extract Data
Once you have the list of evaluations, you must filter for type: survey. CSAT surveys are distinct from QA evaluations (type: evaluation). Within the survey entity, the customer’s rating and comments are stored in specific fields.
from typing import Dict, Any, List
def extract_csat_responses(evaluations: list) -> List[Dict[str, Any]]:
"""
Filters evaluations for CSAT surveys and extracts relevant data.
Args:
evaluations: List of Evaluation entities from the API.
Returns:
List of dictionaries containing CSAT data.
"""
csat_results = []
for eval_item in evaluations:
# Filter for survey type
if eval_item.type != "survey":
continue
# Extract core data
result = {
"conversation_id": eval_item.conversation_id,
"survey_id": eval_item.id,
"created_date": eval_item.created_date,
"customer_rating": None,
"customer_comment": None,
"survey_response": {}
}
# CSAT data is often nested in the 'survey_response' object
# Depending on the survey version, the structure may vary slightly
if hasattr(eval_item, 'survey_response') and eval_item.survey_response:
survey_resp = eval_item.survey_response
# Attempt to get the rating (often a score 1-5 or 1-10)
# The field name can vary based on survey design, but 'score' or 'rating' is common
if hasattr(survey_resp, 'score'):
result["customer_rating"] = survey_resp.score
elif hasattr(survey_resp, 'rating'):
result["customer_rating"] = survey_resp.rating
# Attempt to get the comment
if hasattr(survey_resp, 'comment'):
result["customer_comment"] = survey_resp.comment
elif hasattr(survey_resp, 'comments'):
result["customer_comment"] = survey_resp.comments
# Store the full survey response object for debugging
result["survey_response"] = survey_resp
csat_results.append(result)
return csat_results
Step 4: Handle Edge Cases and Missing Data
Not every survey response will have a rating or a comment. Some customers may submit partial responses. The code above handles None values gracefully. Additionally, ensure you check if the conversation_id in the evaluation matches your original input list, as the API returns all evaluations for those conversations, including QA evaluations.
Complete Working Example
This script ties all components together. It accepts a list of conversation IDs, queries the Quality API, filters for CSAT surveys, and prints the results.
import os
import sys
from datetime import datetime, timezone, timedelta
from genesyscloud.configuration import Configuration
from genesyscloud.api_client import ApiClient
from genesyscloud.quality.api.evaluations_api import EvaluationsApi
from genesyscloud.rest import ApiException
def get_api_client():
"""Initializes the API Client."""
configuration = Configuration(
host="https://api.mypurecloud.com",
client_id=os.getenv("GENESYS_CLIENT_ID"),
client_secret=os.getenv("GENESYS_CLIENT_SECRET")
)
return ApiClient(configuration=configuration)
def build_query_params(conversation_ids: list, start_date: datetime, end_date: datetime) -> dict:
"""Builds query parameters for the evaluations API."""
return {
"conversationIds": ",".join(conversation_ids),
"fromDate": start_date.isoformat(),
"toDate": end_date.isoformat(),
"pageSize": 100
}
def fetch_all_evaluations(api_client: ApiClient, params: dict) -> list:
"""Fetches evaluations with pagination handling."""
evaluations_api = EvaluationsApi(api_client)
all_evaluations = []
page = 1
while True:
try:
response = evaluations_api.post_quality_conversations_evaluations(
_from_date=params["fromDate"],
_to_date=params["toDate"],
conversation_ids=params["conversationIds"],
page_size=params["pageSize"],
page=page
)
if not response.entities:
break
all_evaluations.extend(response.entities)
if response.next_page is None:
break
page += 1
except ApiException as e:
print(f"API Error: {e.status} - {e.reason}")
if e.status == 429:
import time
time.sleep(10)
continue
raise e
return all_evaluations
def extract_csat_data(evaluations: list) -> list:
"""Filters for CSAT surveys and extracts data."""
csat_results = []
for eval_item in evaluations:
if eval_item.type != "survey":
continue
result = {
"conversation_id": eval_item.conversation_id,
"eval_id": eval_item.id,
"created_at": eval_item.created_date,
"rating": None,
"comment": None
}
if hasattr(eval_item, 'survey_response') and eval_item.survey_response:
sr = eval_item.survey_response
if hasattr(sr, 'score'):
result["rating"] = sr.score
elif hasattr(sr, 'rating'):
result["rating"] = sr.rating
if hasattr(sr, 'comment'):
result["comment"] = sr.comment
elif hasattr(sr, 'comments'):
result["comment"] = sr.comments
csat_results.append(result)
return csat_results
def main():
# 1. Setup
api_client = get_api_client()
# 2. Define Inputs
# Replace with actual Conversation IDs
target_conversation_ids = [
"12345678-1234-1234-1234-123456789012",
"87654321-4321-4321-4321-210987654321"
]
# Define date range (last 30 days)
end_date = datetime.now(timezone.utc)
start_date = end_date - timedelta(days=30)
# 3. Build Params
params = build_query_params(target_conversation_ids, start_date, end_date)
# 4. Fetch Data
print(f"Fetching evaluations for {len(target_conversation_ids)} conversations...")
evaluations = fetch_all_evaluations(api_client, params)
print(f"Retrieved {len(evaluations)} total evaluations.")
# 5. Filter and Extract
csat_responses = extract_csat_data(evaluations)
print(f"Found {len(csat_responses)} CSAT survey responses.")
# 6. Output Results
for csat in csat_responses:
print("-" * 40)
print(f"Conversation ID: {csat['conversation_id']}")
print(f"Eval ID: {csat['eval_id']}")
print(f"Created At: {csat['created_at']}")
print(f"Rating: {csat['rating']}")
print(f"Comment: {csat['comment'] if csat['comment'] else 'N/A'}")
print("-" * 40)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
- Cause: The OAuth client lacks the required scope.
- Fix: Go to Admin > Security > OAuth Clients. Select your client. Ensure
quality:evaluation:readis checked. If using a Service Account, ensure the user associated with the service account has the “Quality Analyst” or “Quality Manager” role with permission to view evaluations.
Error: 429 Too Many Requests
- Cause: You exceeded the API rate limit. The Quality API has specific rate limits that are lower than the general platform limits.
- Fix: Implement exponential backoff. The example code includes a basic 10-second sleep on 429 errors. For production systems, use a library like
tenacityto handle retries with jitter.
Error: Empty Results Despite Known CSAT
- Cause: Date range mismatch or Conversation ID format.
- Fix:
- Verify the
fromDateandtoDatecover the period when the survey was completed. Note: The evaluation is created when the survey is submitted, not when the interaction ended. - Ensure Conversation IDs are UUIDs (36 characters including hyphens).
- Check if the survey is archived. The API may not return archived evaluations by default unless specific flags are used.
- Verify the
Error: Attribute Error on survey_response
- Cause: The SDK model structure varies slightly between Genesys Cloud regions or SDK versions.
- Fix: Inspect the raw JSON response from the API using Postman or curl. Map the JSON keys to the SDK object attributes. If the SDK is outdated, update the
genesys-cloud-purecloud-platform-clientpackage.