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 usesrequestsandflask. - 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:executeis not required for the Data Action itself, but the Application User creating the Data Action needsdata:action:write. - An external API key for the target REST service (e.g., OpenWeatherMap API key).
- An Application User with the scope
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.
-
Log in to Genesys Cloud.
-
Navigate to Admin → Integrations → Data Actions.
-
Click Add Data Action.
-
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
- Name:
-
Input Schema:
Add a field:- Name:
city_name - Type:
String - Required:
True
- Name:
-
Output Schema:
Add fields:- Name:
temperature - Type:
Number - Name:
condition - Type:
String - Name:
error_message - Type:
String
- Name:
-
Headers:
If you implemented strict JWT validation, ensure the Genesys Cloud platform is sending the token. By default, Genesys Cloud sends theAuthorizationheader with the JWT for Data Actions. No additional configuration is usually needed unless you are using a custom header for API keys. -
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:
- Ensure your Genesys Cloud instance URL is correct in the
JWKS_URL. Note thatapi.mypurecloud.comis the default, but some regions useapi.eu.purecloud.comorapi.ap.purecloud.com. - Check the server logs for the specific
jwt.InvalidTokenErrormessage. - If you are testing locally using
ngrok, ensure thengrokURL 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:
- In Architect, check the Data Action step configuration. Ensure the Input field is mapped to a valid flow variable that contains a string value.
- Add a Set Variables step before the Data Action to explicitly set
city_nameto 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:
- Increase the
timeoutparameter inhttp_requests.get()if the external API is slow. - Check the Flask server logs for tracebacks.
- 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:
- If the Data Action output schema defines
temperature, the JSON response must be{"temperature": 22.5}. - If you return
{"temp": 22.5}, Genesys Cloud will set thetemperaturevariable tonullor fail the step depending on configuration. - 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.