Building a Custom Data Action for Genesys Cloud Architect using Python

Building a Custom Data Action for Genesys Cloud Architect using Python

What You Will Build

  • You will build a Python FastAPI microservice that acts as a Data Action endpoint for Genesys Cloud Architect.
  • You will implement logic to accept a JSON payload from Architect, call an external REST API, and transform the response.
  • You will configure the Genesys Cloud platform to invoke this service and map the returned data to conversation variables.

Prerequisites

  • Genesys Cloud Account: You need a developer or admin account with permissions to create Data Actions (dataaction:custom:write).
  • Python Environment: Python 3.9+ installed with pip.
  • Dependencies: fastapi, uvicorn, httpx, pydantic.
  • External API: For this tutorial, we will use the JSONPlaceholder API as a mock external service. It requires no authentication and returns predictable JSON.
  • Hosting: A way to host your Python service publicly (e.g., AWS EC2, Heroku, Render, or a local tunnel like ngrok for testing).

Authentication Setup

Genesys Cloud Data Actions use a specific authentication model. Unlike standard API calls where your application holds a bearer token, the Architect flow sends a request to your endpoint with a specific header containing the user’s context.

Your Data Action endpoint does not need to authenticate with Genesys Cloud to receive the call. However, if your Python service needs to call back into Genesys Cloud APIs (e.g., to update a contact record), you must implement OAuth 2.0 Client Credentials flow.

For this tutorial, we focus on the inbound call from Architect. Architect sends a X-Genesys-Auth-Token header. You must validate this token to ensure the request is legitimate.

Validating the Genesys Cloud Token

You can validate the token by sending it to the Genesys Cloud introspection endpoint.

import httpx
import os

GENESYS_ORG_ID = os.getenv("GENESYS_ORG_ID")
GENESYS_REGION = os.getenv("GENESYS_REGION", "us-east-1") # Default to us-east-1

def get_genesis_base_url(region: str) -> str:
    if region == "us-east-1":
        return "https://api.mypurecloud.com"
    elif region == "us-east-2":
        return "https://api.mypurecloud.com" # Alias
    elif region == "eu-west-1":
        return "https://api.eu.purecloud.com"
    # Add other regions as needed
    return "https://api.mypurecloud.com"

async def validate_genesys_token(token: str) -> bool:
    """
    Validates the X-Genesys-Auth-Token received from Architect.
    Returns True if valid, False otherwise.
    """
    base_url = get_genesis_base_url(GENESYS_REGION)
    introspect_url = f"{base_url}/api/v2/oauth2/introspect"
    
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}"
    }
    
    payload = {
        "token": token
    }

    async with httpx.AsyncClient() as client:
        try:
            response = await client.post(
                introspect_url,
                headers=headers,
                json=payload,
                timeout=10.0
            )
            # A 200 OK with active=true means the token is valid
            if response.status_code == 200:
                data = response.json()
                return data.get("active", False)
            return False
        except Exception as e:
            print(f"Token validation error: {e}")
            return False

Note: In production, you should cache the validation result for the duration of the token’s expiration (usually 1 hour) to avoid hitting the Genesys API on every single Architect call.

Implementation

Step 1: Define the FastAPI Application and Schema

We will use FastAPI because it automatically generates OpenAPI documentation and handles JSON parsing efficiently. We need to define the schema for the input coming from Architect and the output going back.

Architect sends a JSON body with a data object containing the inputs you defined in the Data Action configuration.

from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel, Field
from typing import Optional
import httpx
import os
import logging

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(title="Genesys Cloud Data Action Example")

# Define the input schema based on what Architect will send
class ArchitectInput(BaseModel):
    # This matches the 'name' field you will define in Genesys Cloud Admin
    customer_id: int = Field(..., description="The ID of the customer to look up")

# Define the output schema that Architect will map to variables
class ArchitectOutput(BaseModel):
    # These keys will be available in Architect as variables
    full_name: str
    email: str
    phone: str
    is_premium_member: bool

Step 2: Implement the Core Logic (External API Call)

This is the core of the Data Action. It receives the customer_id, calls jsonplaceholder.typicode.com, and transforms the response.

EXTERNAL_API_BASE = "https://jsonplaceholder.typicode.com"

async def fetch_external_data(customer_id: int) -> dict:
    """
    Calls the external REST API and returns the raw JSON.
    Handles timeouts and HTTP errors.
    """
    url = f"{EXTERNAL_API_BASE}/users/{customer_id}"
    
    async with httpx.AsyncClient(timeout=10.0) as client:
        try:
            response = await client.get(url)
            
            # Raise an exception for 4xx/5xx status codes
            response.raise_for_status()
            
            return response.json()
            
        except httpx.HTTPStatusError as e:
            logger.error(f"HTTP Error calling external API: {e.response.status_code}")
            raise HTTPException(
                status_code=e.response.status_code,
                detail=f"External API failed: {e.response.status_code}"
            )
        except httpx.TimeoutException:
            logger.error("Timeout calling external API")
            raise HTTPException(
                status_code=504,
                detail="External API timed out"
            )
        except httpx.RequestError as e:
            logger.error(f"Network error calling external API: {e}")
            raise HTTPException(
                status_code=502,
                detail="Could not connect to external API"
            )

def transform_external_data(raw_data: dict) -> ArchitectOutput:
    """
    Maps the external API response to the Genesys Cloud variable schema.
    """
    # Map fields from JSONPlaceholder to our desired output structure
    # Note: JSONPlaceholder users don't have a 'premium' field, so we mock it
    return ArchitectOutput(
        full_name=raw_data.get("name", "Unknown User"),
        email=raw_data.get("email", "unknown@example.com"),
        phone=raw_data.get("phone", "000-000-0000"),
        is_premium_member=False # Mock logic: in real life, check a DB
    )

Step 3: Create the Data Action Endpoint

This endpoint handles the request from Genesys Cloud. It validates the token, parses the input, calls the external service, and returns the result in the exact format Genesys expects.

Genesys Cloud expects the response body to contain a data object with the output variables.

@app.post("/data-action/customer-lookup")
async def customer_lookup(request: Request):
    """
    Endpoint invoked by Genesys Cloud Architect Data Action.
    """
    
    # 1. Validate the Genesys Cloud Authentication Token
    auth_token = request.headers.get("X-Genesys-Auth-Token")
    
    if not auth_token:
        raise HTTPException(status_code=401, detail="Missing X-Genesys-Auth-Token header")
        
    is_valid = await validate_genesys_token(auth_token)
    if not is_valid:
        raise HTTPException(status_code=403, detail="Invalid Genesys Cloud Auth Token")

    # 2. Parse the Incoming JSON Payload
    try:
        body = await request.json()
        # Architect sends inputs inside the 'data' key
        input_data = body.get("data")
        
        if not input_data:
            raise HTTPException(status_code=400, detail="Missing 'data' in request body")
            
        # Parse using Pydantic for validation
        architect_input = ArchitectInput(**input_data)
        
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Invalid input format: {str(e)}")

    # 3. Execute the Business Logic
    try:
        raw_data = await fetch_external_data(architect_input.customer_id)
        output_data = transform_external_data(raw_data)
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Unexpected error in data action: {e}")
        raise HTTPException(status_code=500, detail="Internal server error")

    # 4. Return the Result in Genesys Cloud Format
    # The response MUST have a 'data' key containing the variables
    return {
        "data": {
            "full_name": output_data.full_name,
            "email": output_data.email,
            "phone": output_data.phone,
            "is_premium_member": output_data.is_premium_member
        }
    }

Step 4: Configure Genesys Cloud Admin

Now that the code is ready, you must configure the Data Action in Genesys Cloud.

  1. Host the Service: Run your FastAPI app. For local testing, use ngrok.

    uvicorn main:app --host 0.0.0.0 --port 8000
    ngrok http 8000
    

    Copy the HTTPS URL (e.g., https://abc123.ngrok.io).

  2. Create the Data Action:

    • Log in to Genesys Cloud Admin.
    • Navigate to Admin > Developers > Data actions.
    • Click Create data action.
    • Name: Customer Lookup
    • Description: Fetches customer details from external API
    • Endpoint URL: Paste your ngrok URL + /data-action/customer-lookup (e.g., https://abc123.ngrok.io/data-action/customer-lookup)
    • Request Method: POST
  3. Define Input Variables:

    • Click Add input variable.
    • Name: customer_id
    • Type: Integer
    • Description: The external customer ID
  4. Define Output Variables:

    • Click Add output variable.
    • Name: full_name
    • Type: String
    • Name: email
    • Type: String
    • Name: phone
    • Type: String
    • Name: is_premium_member
    • Type: Boolean
  5. Save the Data Action.

Step 5: Use in Architect

  1. Open Architect.
  2. Drag the Data Action node onto your flow.
  3. Select Customer Lookup from the dropdown.
  4. Configure Inputs:
    • Set customer_id to a dynamic variable (e.g., customer.id or a prompt result).
  5. Configure Outputs:
    • Architect automatically creates variables for the outputs you defined.
    • You can now use dataAction.customer_lookup.full_name in subsequent nodes (e.g., a Prompt or Set Variables node).

Complete Working Example

Below is the complete main.py file. Save this as main.py.

import os
import logging
from typing import Optional
from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel, Field
import httpx

# --- Configuration ---
GENESYS_ORG_ID = os.getenv("GENESYS_ORG_ID", "your-org-id")
GENESYS_REGION = os.getenv("GENESYS_REGION", "us-east-1")
EXTERNAL_API_BASE = "https://jsonplaceholder.typicode.com"

# --- Logging ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# --- Pydantic Models ---
class ArchitectInput(BaseModel):
    customer_id: int = Field(..., description="The ID of the customer to look up")

class ArchitectOutput(BaseModel):
    full_name: str
    email: str
    phone: str
    is_premium_member: bool

# --- FastAPI App ---
app = FastAPI(title="Genesys Cloud Data Action Example")

def get_genesis_base_url(region: str) -> str:
    """Returns the Genesys Cloud API base URL for the given region."""
    regions = {
        "us-east-1": "https://api.mypurecloud.com",
        "us-east-2": "https://api.mypurecloud.com",
        "eu-west-1": "https://api.eu.purecloud.com",
        "ap-southeast-2": "https://api.au.purecloud.com"
    }
    return regions.get(region, "https://api.mypurecloud.com")

async def validate_genesys_token(token: str) -> bool:
    """
    Validates the X-Genesys-Auth-Token received from Architect.
    """
    base_url = get_genesis_base_url(GENESYS_REGION)
    introspect_url = f"{base_url}/api/v2/oauth2/introspect"
    
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}"
    }
    
    payload = {"token": token}

    async with httpx.AsyncClient() as client:
        try:
            response = await client.post(
                introspect_url,
                headers=headers,
                json=payload,
                timeout=10.0
            )
            if response.status_code == 200:
                data = response.json()
                return data.get("active", False)
            return False
        except Exception as e:
            logger.error(f"Token validation error: {e}")
            return False

async def fetch_external_data(customer_id: int) -> dict:
    """
    Calls the external REST API and returns the raw JSON.
    """
    url = f"{EXTERNAL_API_BASE}/users/{customer_id}"
    
    async with httpx.AsyncClient(timeout=10.0) as client:
        try:
            response = await client.get(url)
            response.raise_for_status()
            return response.json()
            
        except httpx.HTTPStatusError as e:
            logger.error(f"HTTP Error calling external API: {e.response.status_code}")
            raise HTTPException(
                status_code=e.response.status_code,
                detail=f"External API failed: {e.response.status_code}"
            )
        except httpx.TimeoutException:
            logger.error("Timeout calling external API")
            raise HTTPException(
                status_code=504,
                detail="External API timed out"
            )
        except httpx.RequestError as e:
            logger.error(f"Network error calling external API: {e}")
            raise HTTPException(
                status_code=502,
                detail="Could not connect to external API"
            )

def transform_external_data(raw_data: dict) -> ArchitectOutput:
    """
    Maps the external API response to the Genesys Cloud variable schema.
    """
    return ArchitectOutput(
        full_name=raw_data.get("name", "Unknown User"),
        email=raw_data.get("email", "unknown@example.com"),
        phone=raw_data.get("phone", "000-000-0000"),
        is_premium_member=False
    )

@app.post("/data-action/customer-lookup")
async def customer_lookup(request: Request):
    """
    Endpoint invoked by Genesys Cloud Architect Data Action.
    """
    
    # 1. Validate Token
    auth_token = request.headers.get("X-Genesys-Auth-Token")
    
    if not auth_token:
        raise HTTPException(status_code=401, detail="Missing X-Genesys-Auth-Token header")
        
    is_valid = await validate_genesys_token(auth_token)
    if not is_valid:
        raise HTTPException(status_code=403, detail="Invalid Genesys Cloud Auth Token")

    # 2. Parse Input
    try:
        body = await request.json()
        input_data = body.get("data")
        
        if not input_data:
            raise HTTPException(status_code=400, detail="Missing 'data' in request body")
            
        architect_input = ArchitectInput(**input_data)
        
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Invalid input format: {str(e)}")

    # 3. Execute Logic
    try:
        raw_data = await fetch_external_data(architect_input.customer_id)
        output_data = transform_external_data(raw_data)
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Unexpected error in data action: {e}")
        raise HTTPException(status_code=500, detail="Internal server error")

    # 4. Return Result
    return {
        "data": {
            "full_name": output_data.full_name,
            "email": output_data.email,
            "phone": output_data.phone,
            "is_premium_member": output_data.is_premium_member
        }
    }

Common Errors & Debugging

Error: 401 Unauthorized or 403 Forbidden

  • Cause: The X-Genesys-Auth-Token header is missing, expired, or invalid.
  • Fix: Ensure your Data Action configuration in Genesys Cloud has the correct URL. Check your Python logs to see if the token validation step is failing. If you are testing locally with ngrok, ensure the ngrok tunnel is active and the URL is correct.

Error: 400 Bad Request - “Missing ‘data’ in request body”

  • Cause: The request body sent by Architect does not match the expected structure.
  • Fix: Verify that you defined the input variables in the Genesys Cloud Admin console correctly. Architect always wraps inputs in a data object. Ensure your Python code accesses body.get("data").

Error: 504 Gateway Timeout

  • Cause: The external API call took too long. Genesys Cloud has a timeout for Data Actions (typically 30 seconds).
  • Fix: Increase the timeout in your httpx.AsyncClient if appropriate, or optimize the external API call. Add retry logic for transient failures.

Error: Variable Mapping Fails in Architect

  • Cause: The JSON keys returned by your Python service do not exactly match the output variable names defined in Genesys Cloud Admin.
  • Fix: Check the ArchitectOutput model and the return statement in your FastAPI endpoint. Ensure the keys (full_name, email, etc.) match the names you entered in the Admin console.

Official References