Query CSAT Survey Responses via Genesys Cloud Quality API

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/evaluations endpoint with filtering logic applied to the response payload.
  • The tutorial covers Python implementation using the official genesys-cloud-purecloud-platform-client SDK.

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-client v120.0.0 or later.
  • Language/Runtime: Python 3.8+
  • External Dependencies:
    • genesys-cloud-purecloud-platform-client
    • pydantic (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:read is 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 tenacity to handle retries with jitter.

Error: Empty Results Despite Known CSAT

  • Cause: Date range mismatch or Conversation ID format.
  • Fix:
    1. Verify the fromDate and toDate cover the period when the survey was completed. Note: The evaluation is created when the survey is submitted, not when the interaction ended.
    2. Ensure Conversation IDs are UUIDs (36 characters including hyphens).
    3. Check if the survey is archived. The API may not return archived evaluations by default unless specific flags are used.

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-client package.

Official References