Extracting CSAT Survey Responses Tied to Interactions via the Genesys Cloud Quality API
What You Will Build
- You will build a Python script that queries the Genesys Cloud Quality API to retrieve specific survey responses and their associated interaction details.
- This tutorial uses the
PureCloudPlatformClientV2Python SDK to interact with the/api/v2/quality/surveys/responsesand/api/v2/quality/surveys/interactionsendpoints. - The code is written in Python 3.9+ using the
requestslibrary for fallback HTTP calls and the official Genesys Cloud SDK for structured data handling.
Prerequisites
- OAuth Client Type: A Confidential Client (Client Credentials Grant) or Public Client (Authorization Code Grant). For automated scripts, Client Credentials is recommended.
- Required Scopes:
quality:response:read(To read survey responses)quality:interaction:read(To read interaction details linked to surveys)analytics:conversation:read(Optional, if you need deeper conversation analytics context)
- SDK Version: Genesys Cloud Python SDK
v2(latest stable). - Runtime Requirements: Python 3.9 or higher.
- Dependencies:
pip install genesyscloud-python pip install requests pip install python-dotenv
Authentication Setup
Genesys Cloud uses OAuth 2.0. For server-to-server applications, the Client Credentials flow is the standard. You must cache the access token and handle refresh tokens if using Authorization Code, but Client Credentials tokens are short-lived and require re-authentication.
Create a .env file with your credentials:
GENESYS_CLOUD_REGION=us-east-1
GENESYS_CLOUD_CLIENT_ID=your_client_id
GENESYS_CLOUD_CLIENT_SECRET=your_client_secret
Initialize the SDK client. The SDK handles the initial token fetch and basic retry logic for transient errors.
import os
from dotenv import load_dotenv
from purecloudplatformclientv2 import PureCloudPlatformClientV2, ApiClient, Configuration
load_dotenv()
def get_purecloud_client() -> PureCloudPlatformClientV2:
"""
Initializes and returns a configured PureCloudPlatformClientV2 instance.
"""
region = os.getenv("GENESYS_CLOUD_REGION")
client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
if not all([region, client_id, client_secret]):
raise ValueError("Missing required environment variables for Genesys Cloud authentication.")
# The SDK automatically determines the host based on the region
configuration = Configuration(
host=f"https://{region}.mygen.com",
client_id=client_id,
client_secret=client_secret
)
client = PureCloudPlatformClientV2(configuration)
return client
# Instantiate the client
client = get_purecloud_client()
Implementation
Step 1: Querying Survey Responses
The Quality API does not provide a single endpoint that returns “Survey Response + Interaction Data” in one go. You must first query the survey responses, then use the interaction IDs from those responses to fetch the specific interaction details.
The endpoint /api/v2/quality/surveys/responses supports filtering by date range, survey ID, and evaluation ID. It is paginated.
Required Scope: quality:response:read
from purecloudplatformclientv2 import QualityApi, QuerySurveyResponseRequest
from datetime import datetime, timedelta
from purecloudplatformclientv2.rest import ApiException
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def fetch_survey_responses(quality_api: QualityApi, start_date: datetime, end_date: datetime) -> list:
"""
Fetches all survey responses within a date range with pagination.
"""
all_responses = []
# Construct the query parameters
# start and end must be ISO 8601 format strings
query_body = {
"start": start_date.isoformat(),
"end": end_date.isoformat(),
"size": 100, # Maximum page size
"expand": ["interaction"] # Optional: Try to expand interaction data if supported by specific API version
}
try:
while True:
# The SDK method maps to POST /api/v2/quality/surveys/responses/query
response = quality_api.post_quality_survey_responses_query(
body=query_body
)
if response.entities and len(response.entities) > 0:
all_responses.extend(response.entities)
logger.info(f"Retrieved {len(response.entities)} responses. Total so far: {len(all_responses)}")
# Check for pagination
if not response.next_uri:
break
# The SDK does not automatically follow next_uri in this specific query method
# in some versions, so we manually request the next page if the SDK doesn't handle it.
# However, the PureCloud SDK usually returns a 'next_uri'. We need to make a direct HTTP call
# or use the SDK's pagination helper if available.
# For robustness, we will use the next_uri directly via requests if the SDK method doesn't support it.
# Note: The SDK's post_quality_survey_responses_query returns a SurveyResponseQueryResponse object.
# If the SDK version supports automatic pagination, this loop handles it.
# If not, we break and rely on the user to implement next_uri fetching.
# In current SDK versions, we often need to fetch the next page manually.
if response.next_uri:
# To keep this tutorial SDK-centric, we assume the developer might need to
# switch to raw HTTP for complex pagination or use a wrapper.
# Here we stop after the first page for simplicity in the core logic,
# but a production script MUST handle pagination.
logger.warning("Pagination detected. Implementing full pagination requires handling next_uri manually.")
break
else:
break
except ApiException as e:
logger.error(f"Error fetching survey responses: {e}")
if e.status == 401:
logger.error("Authentication failed. Check your token.")
elif e.status == 403:
logger.error("Forbidden. Ensure your client has 'quality:response:read' scope.")
raise
return all_responses
# Example usage
quality_api = QualityApi(client)
start_dt = datetime.utcnow() - timedelta(days=7)
end_dt = datetime.utcnow()
responses = fetch_survey_responses(quality_api, start_dt, end_dt)
Expected Response Structure (JSON):
{
"entities": [
{
"id": "response-uuid-123",
"surveyId": "survey-uuid-456",
"interactionId": "interaction-uuid-789",
"externalInteractionId": "ext-interaction-id",
"score": 5,
"responseText": "Great service!",
"receivedDate": "2023-10-27T10:00:00Z",
"interaction": {
"id": "interaction-uuid-789",
"type": "voice",
"startTime": "2023-10-27T09:55:00Z"
}
}
],
"nextUri": null
}
Step 2: Extracting Interaction Details
The interaction object in the survey response is often lightweight. To get the full context (agent ID, queue ID, duration, wrap-up code), you must query the Quality Interaction API.
Required Scope: quality:interaction:read
The endpoint /api/v2/quality/surveys/interactions allows you to fetch multiple interactions at once by passing a list of IDs. This is more efficient than fetching them one by one.
from purecloudplatformclientv2 import GetSurveyInteractionRequest
import uuid
def fetch_interaction_details(quality_api: QualityApi, interaction_ids: list[str]) -> dict:
"""
Fetches detailed interaction data for a list of interaction IDs.
Returns a dictionary mapping interaction_id to interaction details.
"""
if not interaction_ids:
return {}
# The API allows fetching up to 1000 interactions at once
# We chunk the IDs to stay within limits
chunk_size = 100
all_interactions = {}
for i in range(0, len(interaction_ids), chunk_size):
chunk = interaction_ids[i:i + chunk_size]
try:
# POST /api/v2/quality/surveys/interactions
# The SDK method is post_quality_survey_interactions
response = quality_api.post_quality_survey_interactions(
body={"ids": chunk}
)
if response.entities:
for interaction in response.entities:
all_interactions[interaction.id] = interaction
except ApiException as e:
logger.error(f"Error fetching interactions for chunk starting at index {i}: {e}")
if e.status == 404:
logger.warning("Some interaction IDs may no longer exist or be accessible.")
raise
return all_interactions
# Example usage
interaction_ids = [resp.interaction.id for resp in responses if resp.interaction and resp.interaction.id]
interaction_details = fetch_interaction_details(quality_api, interaction_ids)
Step 3: Correlating and Processing Results
Now that you have the survey responses and the detailed interaction objects, you must join them in memory. This step is where you apply business logic, such as filtering for low scores or specific agents.
def process_survey_data(responses: list, interaction_details: dict) -> list[dict]:
"""
Correlates survey responses with interaction details and returns a structured list.
"""
enriched_data = []
for response in responses:
interaction_id = response.interaction.id if response.interaction else None
if not interaction_id:
logger.warning(f"Survey response {response.id} has no interaction ID. Skipping.")
continue
interaction = interaction_details.get(interaction_id)
if not interaction:
logger.warning(f"Could not find interaction details for ID {interaction_id}. Skipping.")
continue
# Extract key fields
enriched_record = {
"survey_id": response.id,
"score": response.score,
"comments": response.responseText,
"received_date": response.receivedDate,
"interaction_type": interaction.type,
"start_time": interaction.startTime,
"agent_id": interaction.agentId if hasattr(interaction, 'agentId') else None,
"queue_id": interaction.queueId if hasattr(interaction, 'queueId') else None,
"duration_seconds": interaction.duration if hasattr(interaction, 'duration') else None
}
enriched_data.append(enriched_record)
return enriched_data
enriched_surveys = process_survey_data(responses, interaction_details)
# Print a sample
if enriched_surveys:
print(f"Processed {len(enriched_surveys)} surveys.")
print(f"Sample record: {enriched_surveys[0]}")
Complete Working Example
This script combines authentication, pagination handling (simplified for clarity, but robust), and data correlation.
import os
import logging
from datetime import datetime, timedelta
from dotenv import load_dotenv
from purecloudplatformclientv2 import PureCloudPlatformClientV2, QualityApi
from purecloudplatformclientv2.rest import ApiException
import requests
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
load_dotenv()
def get_client():
region = os.getenv("GENESYS_CLOUD_REGION")
client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
if not all([region, client_id, client_secret]):
raise ValueError("Missing env vars: GENESYS_CLOUD_REGION, CLIENT_ID, CLIENT_SECRET")
from purecloudplatformclientv2 import Configuration
config = Configuration(
host=f"https://{region}.mygen.com",
client_id=client_id,
client_secret=client_secret
)
return PureCloudPlatformClientV2(config)
def fetch_all_responses(quality_api, start_date, end_date):
"""
Handles pagination manually using the next_uri from the response.
"""
all_responses = []
query_body = {
"start": start_date.isoformat(),
"end": end_date.isoformat(),
"size": 100
}
# Use the SDK for the first call to get the initial response and next_uri
try:
response = quality_api.post_quality_survey_responses_query(body=query_body)
except ApiException as e:
logger.error(f"Initial query failed: {e}")
return []
if response.entities:
all_responses.extend(response.entities)
# Handle pagination using raw requests if SDK doesn't auto-follow next_uri
# This is a common pattern when SDK pagination helpers are not exposed for complex queries
next_uri = response.next_uri
while next_uri:
try:
# We need the access token from the client configuration
auth_client = quality_api.client.get_access_token()
headers = {
"Authorization": f"Bearer {auth_client}",
"Content-Type": "application/json"
}
# The next_uri is a full URL
res = requests.post(next_uri, headers=headers, json={}) # POST with empty body for next page
if res.status_code == 200:
data = res.json()
if data.get("entities"):
all_responses.extend(data["entities"])
logger.info(f"Fetched another {len(data['entities'])} responses.")
next_uri = data.get("nextUri")
else:
logger.error(f"Failed to fetch next page: {res.status_code} - {res.text}")
break
except Exception as e:
logger.error(f"Error during pagination: {e}")
break
return all_responses
def main():
try:
client = get_client()
quality_api = QualityApi(client)
# Define date range: Last 7 days
end_dt = datetime.utcnow()
start_dt = end_dt - timedelta(days=7)
logger.info(f"Fetching survey responses from {start_dt.isoformat()} to {end_dt.isoformat()}")
# Step 1: Get Responses
responses = fetch_all_responses(quality_api, start_dt, end_dt)
logger.info(f"Total responses fetched: {len(responses)}")
if not responses:
logger.info("No survey responses found in the specified range.")
return
# Step 2: Get Interaction IDs
interaction_ids = set()
for resp in responses:
if resp.interaction and resp.interaction.id:
interaction_ids.add(resp.interaction.id)
logger.info(f"Fetching details for {len(interaction_ids)} unique interactions.")
# Step 3: Get Interaction Details
interaction_details = {}
chunk_size = 100
id_list = list(interaction_ids)
for i in range(0, len(id_list), chunk_size):
chunk = id_list[i:i+chunk_size]
try:
int_response = quality_api.post_quality_survey_interactions(body={"ids": chunk})
if int_response.entities:
for inter in int_response.entities:
interaction_details[inter.id] = inter
except ApiException as e:
logger.error(f"Error fetching interactions: {e}")
# Step 4: Correlate and Output
enriched = []
for resp in responses:
int_id = resp.interaction.id if resp.interaction else None
if int_id and int_id in interaction_details:
inter = interaction_details[int_id]
enriched.append({
"survey_id": resp.id,
"score": resp.score,
"agent_id": getattr(inter, 'agentId', None),
"queue_id": getattr(inter, 'queueId', None),
"duration": getattr(inter, 'duration', None),
"comments": resp.responseText
})
logger.info(f"Successfully processed {len(enriched)} enriched survey records.")
# Example: Filter for low scores
low_scores = [r for r in enriched if r['score'] is not None and r['score'] < 3]
logger.info(f"Found {len(low_scores)} low-score surveys (score < 3).")
except Exception as e:
logger.critical(f"Fatal error: {e}", exc_info=True)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
Cause: The OAuth token has expired or was never generated correctly.
Fix: Ensure your .env file contains valid CLIENT_ID and CLIENT_SECRET. Verify that the client is active in the Genesys Cloud Admin Console. The SDK handles token generation, but if you are manually managing tokens, ensure you refresh them before every batch of requests.
Error: 403 Forbidden
Cause: The OAuth client lacks the required scopes.
Fix: Go to Admin > Security > OAuth clients. Select your client and add the following scopes:
quality:response:readquality:interaction:read
Save the client and regenerate the token.
Error: 429 Too Many Requests
Cause: You are hitting the API rate limit. The Quality API has specific rate limits per tenant.
Fix: Implement exponential backoff. The requests library in the pagination example above does not automatically retry. In a production environment, use a library like tenacity or implement a retry loop with increasing delays (e.g., 1s, 2s, 4s).
import time
def safe_api_call(func, *args, retries=3, backoff=2):
for attempt in range(retries):
try:
return func(*args)
except ApiException as e:
if e.status == 429:
wait_time = backoff ** attempt
logger.warning(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time)
else:
raise
raise Exception("Max retries exceeded")
Error: Interaction ID Not Found
Cause: The survey response references an interaction that was deleted, expired, or is in a different org/partition.
Fix: Check the interactionId in the survey response. Verify that the interaction exists in the Quality module. If the interaction was purged by data retention policies, it will not be available via the API.