Building a Custom Data Action for Genesys Cloud CX with Python and Flask

Building a Custom Data Action for Genesys Cloud CX with Python and Flask

What You Will Build

  • A secure, stateless Flask microservice that accepts a Genesys Cloud Data Action POST request, queries an external REST API (OpenWeatherMap), and returns a structured JSON response.
  • The logic required to map the external API response fields into specific Genesys Cloud Architect flow variables.
  • A Python implementation demonstrating OAuth2 client credentials validation, input sanitization, and error handling for 4xx/5xx HTTP status codes.

Prerequisites

  • Platform: Genesys Cloud CX (PureCloud)
  • SDK/Library: Genesys Cloud Python SDK (genesys-cloud-py-client) is not required for the service code, but is useful for testing the Data Action via API. The service itself uses requests and flask.
  • Language: Python 3.9+
  • External Dependencies:
    • flask: For the HTTP server.
    • requests: For calling the external API.
    • pyjwt (optional but recommended): For verifying Genesys Cloud JWT tokens if implementing strict header validation.
    • python-dotenv: For managing environment variables.
  • Genesys Cloud Setup:
    • An Application User with the scope data:action:execute is not required for the Data Action itself, but the Application User creating the Data Action needs data:action:write.
    • An external API key for the target REST service (e.g., OpenWeatherMap API key).

Authentication Setup

Genesys Cloud Data Actions are invoked via HTTP POST. The platform sends an Authorization header containing a JWT token issued by Genesys Cloud. While Genesys Cloud does not strictly enforce token validation on the receiving end for public endpoints, production-grade integrations must validate the token to ensure the request originates from your Genesys Cloud instance and not a malicious actor.

The following code snippet demonstrates how to validate the JWT signature using the public key provided by Genesys Cloud.

import jwt
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

# Genesys Cloud JWT Public Key URL
# This key is used to verify the signature of the token sent in the Authorization header
JWKS_URL = "https://api.mypurecloud.com/api/v2/authorization/jwks"

def get_public_key():
    """Fetches the public JWK from Genesys Cloud."""
    response = requests.get(JWKS_URL)
    response.raise_for_status()
    jwks = response.json()
    # In a production app, cache this key and refresh periodically, not on every request
    return jwks['keys'][0]

def verify_genesys_token(token_header):
    """
    Verifies the JWT token sent by Genesys Cloud.
    Returns True if valid, raises an exception if invalid.
    """
    if not token_header or not token_header.startswith("Bearer "):
        raise ValueError("Missing or invalid Authorization header")
    
    token = token_header.split(" ")[1]
    public_key = get_public_key()
    
    try:
        # Verify the token signature and expiry
        payload = jwt.decode(
            token,
            key=public_key,
            algorithms=["RS256"],
            audience="https://api.mypurecloud.com" # Ensure it's for the API audience
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise ValueError("Token has expired")
    except jwt.InvalidTokenError as e:
        raise ValueError(f"Invalid token: {str(e)}")

Implementation

Step 1: Define the Data Action Schema and Endpoint

Genesys Cloud Architect allows you to define the input schema for a Data Action. When the flow executes, it sends a JSON body matching this schema. You must define the expected input fields in your Flask route and handle missing fields gracefully.

For this tutorial, we will build a Data Action called GetWeatherData.

  • Input: city_name (string)
  • Output: temperature (number), condition (string), error_message (string, optional)
import os
from flask import Flask, request, jsonify
import requests as http_requests # Alias to avoid conflict with Flask request
import jwt

app = Flask(__name__)

# Configuration
GENESYS_JWKS_URL = "https://api.mypurecloud.com/api/v2/authorization/jwks"
OPENWEATHER_API_KEY = os.environ.get("OPENWEATHER_API_KEY")
OPENWEATHER_BASE_URL = "https://api.openweathermap.org/data/2.5/weather"

@app.route('/data-action/weather', methods=['POST'])
def handle_weather_data_action():
    """
    Endpoint for the Genesys Cloud Data Action.
    Expects a POST request with a JSON body containing 'city_name'.
    """
    
    # 1. Validate Authentication
    auth_header = request.headers.get('Authorization')
    try:
        verify_genesys_token(auth_header)
    except ValueError as e:
        return jsonify({"error": "Authentication failed", "details": str(e)}), 401

    # 2. Parse Input
    try:
        data = request.get_json()
        if not data:
            return jsonify({"error": "Invalid JSON payload"}), 400
        
        city_name = data.get('city_name')
        if not city_name:
            return jsonify({"error": "Missing required field: city_name"}), 400
            
        # Sanitize input
        city_name = city_name.strip()

    except Exception as e:
        return jsonify({"error": "Input parsing error", "details": str(e)}), 400

    # 3. Execute External Logic
    result = fetch_weather_data(city_name)

    # 4. Return Response
    # Genesys Cloud expects a 2xx response for success.
    # The JSON body structure must match the output schema defined in Architect.
    return jsonify(result), 200

def verify_genesys_token(token_header):
    # Implementation from Authentication Setup section
    # ... (include full implementation here in production)
    pass

Step 2: Implement External API Logic and Error Handling

The core logic involves calling the external REST API. You must handle network errors, HTTP errors from the external provider, and map the response fields to the names expected by Genesys Cloud Architect.

Critical Note: Genesys Cloud Architect variables are case-sensitive. If your Data Action output schema defines a variable named temperature, your JSON response must have the key "temperature".

def fetch_weather_data(city_name: str) -> dict:
    """
    Calls OpenWeatherMap API and maps the response to Genesys Cloud variables.
    """
    if not OPENWEATHER_API_KEY:
        return {
            "temperature": 0,
            "condition": "Configuration Error",
            "error_message": "API Key not configured"
        }

    params = {
        "q": city_name,
        "appid": OPENWEATHER_API_KEY,
        "units": "metric" # Celsius
    }

    try:
        response = http_requests.get(OPENWEATHER_BASE_URL, params=params, timeout=5)
        response.raise_for_status() # Raise HTTPError for bad responses (4xx, 5xx)
        
        data = response.json()
        
        # Map OpenWeatherMap fields to Genesys Cloud Architect variables
        # OpenWeatherMap returns temp in Kelvin by default, we requested metric (Celsius)
        temperature = data.get('main', {}).get('temp')
        description = data.get('weather', [{}])[0].get('description', 'Unknown')
        
        # Capitalize first letter for better readability in IVR prompts
        condition = description.capitalize() if description else "Unknown"

        return {
            "temperature": temperature,
            "condition": condition,
            "error_message": None # Explicitly set to null/None to clear previous errors
        }

    except http_requests.exceptions.HTTPError as http_err:
        # Handle specific HTTP errors from OpenWeatherMap
        error_msg = f"External API Error: {http_err.response.status_code}"
        if http_err.response.status_code == 401:
            error_msg = "Invalid API Key"
        elif http_err.response.status_code == 404:
            error_msg = f"City '{city_name}' not found"
        
        return {
            "temperature": 0,
            "condition": "Error",
            "error_message": error_msg
        }
        
    except http_requests.exceptions.Timeout:
        return {
            "temperature": 0,
            "condition": "Timeout",
            "error_message": "External API request timed out"
        }
        
    except Exception as e:
        # Catch-all for unexpected errors (JSON parsing, network issues)
        return {
            "temperature": 0,
            "condition": "System Error",
            "error_message": str(e)
        }

Step 3: Configure the Data Action in Genesys Cloud Architect

You cannot test the code without defining the Data Action in Genesys Cloud. This step defines the contract between your Flask app and the Architect flow.

  1. Log in to Genesys Cloud.

  2. Navigate to AdminIntegrationsData Actions.

  3. Click Add Data Action.

  4. Fill in the details:

    • Name: GetWeatherData
    • Description: Fetches current weather for a city
    • Endpoint URL: https://your-flask-app.example.com/data-action/weather
    • Method: POST
    • Content Type: application/json
  5. Input Schema:
    Add a field:

    • Name: city_name
    • Type: String
    • Required: True
  6. Output Schema:
    Add fields:

    • Name: temperature
    • Type: Number
    • Name: condition
    • Type: String
    • Name: error_message
    • Type: String
  7. Headers:
    If you implemented strict JWT validation, ensure the Genesys Cloud platform is sending the token. By default, Genesys Cloud sends the Authorization header with the JWT for Data Actions. No additional configuration is usually needed unless you are using a custom header for API keys.

  8. Click Save.

Complete Working Example

Below is the complete app.py file. You must install dependencies via pip install flask requests pyjwt python-dotenv.

import os
import jwt
import requests as http_requests
from flask import Flask, request, jsonify

app = Flask(__name__)

# --- Configuration ---
GENESYS_JWKS_URL = "https://api.mypurecloud.com/api/v2/authorization/jwks"
OPENWEATHER_API_KEY = os.environ.get("OPENWEATHER_API_KEY")
OPENWEATHER_BASE_URL = "https://api.openweathermap.org/data/2.5/weather"

# Cache for JWK to avoid fetching on every request
_jwk_cache = None

def get_public_key():
    global _jwk_cache
    if _jwk_cache:
        return _jwk_cache
    try:
        response = http_requests.get(GENESYS_JWKS_URL, timeout=5)
        response.raise_for_status()
        jwks = response.json()
        _jwk_cache = jwks['keys'][0]
        return _jwk_cache
    except Exception:
        raise ValueError("Failed to fetch Genesys Cloud public key")

def verify_genesys_token(token_header):
    if not token_header or not token_header.startswith("Bearer "):
        raise ValueError("Missing or invalid Authorization header")
    
    token = token_header.split(" ")[1]
    public_key = get_public_key()
    
    try:
        payload = jwt.decode(
            token,
            key=public_key,
            algorithms=["RS256"],
            audience="https://api.mypurecloud.com"
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise ValueError("Token has expired")
    except jwt.InvalidTokenError as e:
        raise ValueError(f"Invalid token: {str(e)}")

@app.route('/data-action/weather', methods=['POST'])
def handle_weather_data_action():
    # 1. Auth Check
    auth_header = request.headers.get('Authorization')
    try:
        verify_genesys_token(auth_header)
    except ValueError as e:
        return jsonify({"error": "Authentication failed", "details": str(e)}), 401

    # 2. Input Validation
    try:
        data = request.get_json()
        if not data:
            return jsonify({"error": "Invalid JSON payload"}), 400
        
        city_name = data.get('city_name')
        if not city_name:
            return jsonify({"error": "Missing required field: city_name"}), 400
            
        city_name = city_name.strip()

    except Exception as e:
        return jsonify({"error": "Input parsing error", "details": str(e)}), 400

    # 3. Business Logic
    result = fetch_weather_data(city_name)

    # 4. Response
    return jsonify(result), 200

def fetch_weather_data(city_name: str) -> dict:
    if not OPENWEATHER_API_KEY:
        return {
            "temperature": 0,
            "condition": "Config Error",
            "error_message": "API Key not configured"
        }

    params = {
        "q": city_name,
        "appid": OPENWEATHER_API_KEY,
        "units": "metric"
    }

    try:
        response = http_requests.get(OPENWEATHER_BASE_URL, params=params, timeout=5)
        response.raise_for_status()
        
        data = response.json()
        temperature = data.get('main', {}).get('temp')
        description = data.get('weather', [{}])[0].get('description', 'Unknown')
        condition = description.capitalize() if description else "Unknown"

        return {
            "temperature": temperature,
            "condition": condition,
            "error_message": None
        }

    except http_requests.exceptions.HTTPError as http_err:
        error_msg = f"External API Error: {http_err.response.status_code}"
        if http_err.response.status_code == 401:
            error_msg = "Invalid API Key"
        elif http_err.response.status_code == 404:
            error_msg = f"City '{city_name}' not found"
        
        return {
            "temperature": 0,
            "condition": "Error",
            "error_message": error_msg
        }
        
    except http_requests.exceptions.Timeout:
        return {
            "temperature": 0,
            "condition": "Timeout",
            "error_message": "External API request timed out"
        }
        
    except Exception as e:
        return {
            "temperature": 0,
            "condition": "System Error",
            "error_message": str(e)
        }

if __name__ == '__main__':
    # For local testing only. Use gunicorn or uwsgi in production.
    app.run(host='0.0.0.0', port=5000, debug=True)

Common Errors & Debugging

Error: 401 Unauthorized (Authentication Failed)

Cause: The JWT token sent by Genesys Cloud is invalid, expired, or the public key used for verification is incorrect.
Fix:

  1. Ensure your Genesys Cloud instance URL is correct in the JWKS_URL. Note that api.mypurecloud.com is the default, but some regions use api.eu.purecloud.com or api.ap.purecloud.com.
  2. Check the server logs for the specific jwt.InvalidTokenError message.
  3. If you are testing locally using ngrok, ensure the ngrok URL is accessible from Genesys Cloud’s servers.

Error: 400 Bad Request (Missing Field)

Cause: The Architect flow did not pass the city_name variable, or the variable was null.
Fix:

  1. In Architect, check the Data Action step configuration. Ensure the Input field is mapped to a valid flow variable that contains a string value.
  2. Add a Set Variables step before the Data Action to explicitly set city_name to a hardcoded value for testing.

Error: 500 Internal Server Error (Timeout)

Cause: The external API (OpenWeatherMap) took longer than 5 seconds to respond, or the Flask server crashed.
Fix:

  1. Increase the timeout parameter in http_requests.get() if the external API is slow.
  2. Check the Flask server logs for tracebacks.
  3. Ensure the external API key is valid. An invalid key often returns a 401, which is handled, but a malformed key might cause unexpected behavior.

Error: Variable Mapping Mismatch

Cause: The JSON key returned by Flask does not match the Output Schema defined in Genesys Cloud.
Fix:

  1. If the Data Action output schema defines temperature, the JSON response must be {"temperature": 22.5}.
  2. If you return {"temp": 22.5}, Genesys Cloud will set the temperature variable to null or fail the step depending on configuration.
  3. Use the Genesys Cloud API Explorer to test the Data Action directly by sending a POST request to the Genesys Cloud Data Action endpoint, which will proxy your request. This helps isolate whether the error is in your Flask app or the Architect mapping.

Official References