Implementing Customer Lookup via Architect GetExternalContactAction in Genesys Cloud CX

Implementing Customer Lookup via Architect GetExternalContactAction in Genesys Cloud CX

What You Will Build

  • A pure Python script that executes a Genesys Cloud Architect flow to resolve a customer profile based on a specific phone number.
  • This tutorial uses the Genesys Cloud Python SDK (genesyscloud) and the REST API for flow execution.
  • The code is written in Python 3.9+ using the httpx library for asynchronous HTTP requests and the official Genesys Cloud SDK for authentication.

Prerequisites

  • OAuth Client Type: Confidential Client (Client Credentials Grant) or JWT. This tutorial uses Client Credentials for simplicity.
  • Required Scopes:
    • flow:execute (to trigger the flow)
    • flow:view (to retrieve flow definition if needed for debugging)
    • architect:flow:execute
  • SDK Version: genesyscloud >= 2.0.0 (Python).
  • Runtime Requirements: Python 3.9 or higher.
  • External Dependencies:
    • genesyscloud: Official Genesys Cloud Python SDK.
    • httpx: Modern async HTTP client for making the execution call.
    • pydantic: For data validation (optional but recommended for robust parsing).
  • Architect Flow Setup: You must have an existing Architect Flow that contains a GetExternalContact action. This action must be configured to look up data based on the PhoneNumber attribute.

Authentication Setup

Genesys Cloud APIs require OAuth 2.0 authentication. The Python SDK handles the token acquisition and refresh logic automatically when configured correctly. You must initialize the Configuration object with your environment, client ID, and client secret.

import os
from genesyscloud.configuration import Configuration

def init_genesys_config() -> Configuration:
    """
    Initializes the Genesys Cloud configuration object.
    Uses environment variables for security.
    """
    # Load environment variables
    region = os.getenv("GENESYS_REGION", "mypurecloud.com")
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")

    if not all([region, client_id, client_secret]):
        raise ValueError("Missing required environment variables: GENESYS_REGION, GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET")

    # Create configuration instance
    config = Configuration(
        host=f"https://api.{region}",
        client_id=client_id,
        client_secret=client_secret
    )

    return config

This configuration object will be used by the SDK to manage the OAuth token lifecycle. You do not need to manually handle token expiration in subsequent API calls if you use the SDK’s built-in client methods.

Implementation

Step 1: Define the Architect Flow Execution Payload

The GetExternalContact action is part of an Architect Flow. To trigger this lookup, you do not call the GetExternalContact API endpoint directly. Instead, you execute the Flow that contains this action. The lookup logic is driven by the attributes passed into the flow execution.

The critical parameter is the Input Attributes. You must map the phone number to the variable name expected by your GetExternalContact action in the Architect UI.

Assume your Architect Flow has a GetExternalContact action configured as follows:

  • Lookup Key: PhoneNumber
  • External Contact Source: CRM_External_Contacts

You must pass the phone number in the attributes JSON body of the flow execution request.

import json
from typing import Dict, Any

def build_flow_execution_payload(phone_number: str, flow_id: str) -> Dict[str, Any]:
    """
    Constructs the JSON payload for executing a Genesys Cloud Flow.
    
    Args:
        phone_number: The E.164 formatted phone number to look up.
        flow_id: The UUID of the Architect Flow containing the GetExternalContact action.
        
    Returns:
        A dictionary representing the execution payload.
    """
    # The 'attributes' object is passed to the flow.
    # The key 'PhoneNumber' must match the input variable defined in the 
    # GetExternalContact action's configuration in Architect.
    payload = {
        "flowId": flow_id,
        "attributes": {
            "PhoneNumber": phone_number,
            "LookupSource": "PrimaryCRM" # Optional context variable
        },
        # Optional: Set a timeout for the flow execution if it involves long-running tasks.
        # For simple lookups, the default is usually sufficient.
        "timeout": 30000 
    }
    
    return payload

Step 2: Execute the Flow via REST API

The Python SDK does not have a direct method for POST /api/v2/flows/execute. You must use the ApiClient to make the raw HTTP request. This approach gives you full control over the headers and allows you to handle the asynchronous nature of flow execution if needed (though for simple lookups, synchronous execution is common).

Note: The GetExternalContact action itself is synchronous within the flow context. The flow will pause at the action, perform the lookup, and continue. If you are triggering this flow from an external system, you are waiting for the flow to complete or return an initial result.

import httpx
from genesyscloud.api_client import ApiClient
from genesyscloud.rest import ApiException
import time

async def execute_flow_lookup(
    api_client: ApiClient, 
    payload: Dict[str, Any], 
    base_url: str
) -> Dict[str, Any]:
    """
    Executes the Architect Flow using the provided payload.
    
    Args:
        api_client: The authenticated Genesys Cloud ApiClient instance.
        payload: The JSON payload containing flowId and attributes.
        base_url: The base API URL (e.g., https://api.mypurecloud.com).
        
    Returns:
        The JSON response from the flow execution.
    """
    endpoint = f"{base_url}/api/v2/flows/execute"
    
    # Get the current access token from the SDK's configuration
    # The SDK manages token refresh, so we fetch the current valid token.
    token = api_client.configuration.get_access_token()
    
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json"
    }
    
    async with httpx.AsyncClient(timeout=30.0) as client:
        try:
            response = await client.post(
                endpoint,
                headers=headers,
                json=payload
            )
            
            # Raise an exception for bad status codes (4xx, 5xx)
            response.raise_for_status()
            
            return response.json()
            
        except httpx.HTTPStatusError as e:
            error_detail = e.response.text
            print(f"HTTP Error {e.response.status_code}: {error_detail}")
            raise ApiException(status=e.response.status_code, reason=error_detail)
        except httpx.RequestError as e:
            print(f"Request Error: {e}")
            raise ApiException(status=500, reason=f"Network error: {e}")

Step 3: Parse the GetExternalContact Result

The response from /api/v2/flows/execute depends heavily on how the flow is configured. If the flow ends with a Set Variable or Return action, the output will be in the output object. If the flow is triggered by an inbound call, the response structure differs.

For a pure API-triggered flow designed for lookup, the response typically contains an id (execution ID) and potentially an output object if the flow completes synchronously. However, Genesys Cloud flow executions are often asynchronous.

Critical Distinction:
If you need the immediate result of the GetExternalContact action, you must ensure the Architect Flow is configured to return the data immediately upon completion. If the flow is long-running, you must poll the GET /api/v2/flows/executions/{executionId} endpoint.

For this tutorial, we assume a synchronous-like return where the flow completes quickly and returns the enriched contact data.

from typing import Optional, List

def extract_contact_data(flow_response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """
    Extracts the customer data from the flow execution response.
    
    The structure of the response depends on the final action of the flow.
    If the flow ends with a 'Return' action, the data is in 'output'.
    """
    if not flow_response:
        return None

    # Check if the response contains an output object
    if "output" in flow_response:
        output = flow_response["output"]
        # The GetExternalContact action populates the 'contact' variable 
        # or whatever variable name was assigned in Architect.
        # We look for a common pattern where the contact details are returned.
        
        # Scenario A: The flow returns a specific variable named 'CustomerProfile'
        if "CustomerProfile" in output:
            return output["CustomerProfile"]
        
        # Scenario B: The flow returns the raw external contact object
        if "externalContact" in output:
            return output["externalContact"]
            
    # If no standard output, log the raw response for debugging
    print(f"Unexpected response structure: {flow_response}")
    return None

Complete Working Example

This script combines authentication, payload creation, execution, and result parsing. It requires the following environment variables:

  • GENESYS_REGION
  • GENESYS_CLIENT_ID
  • GENESYS_CLIENT_SECRET
  • TARGET_FLOW_ID (The UUID of your Architect Flow)
import os
import asyncio
import sys
from genesyscloud.configuration import Configuration
from genesyscloud.api_client import ApiClient
from genesyscloud.rest import ApiException
import httpx

# --- Configuration & Helpers ---

def init_genesys_config() -> Configuration:
    region = os.getenv("GENESYS_REGION", "mypurecloud.com")
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")

    if not all([region, client_id, client_secret]):
        raise ValueError("Missing required environment variables.")

    return Configuration(
        host=f"https://api.{region}",
        client_id=client_id,
        client_secret=client_secret
    )

def build_flow_execution_payload(phone_number: str, flow_id: str) -> dict:
    return {
        "flowId": flow_id,
        "attributes": {
            "PhoneNumber": phone_number,
            "Context": "API_Lookup"
        },
        "timeout": 30000
    }

# --- Core Execution Logic ---

async def lookup_customer_by_phone(
    config: Configuration,
    flow_id: str,
    phone_number: str
) -> dict:
    """
    Main function to trigger the flow and retrieve customer data.
    """
    # 1. Initialize API Client
    api_client = ApiClient(configuration=config)
    
    # 2. Build Payload
    payload = build_flow_execution_payload(phone_number, flow_id)
    
    # 3. Determine Base URL
    base_url = config.host
    
    # 4. Execute Flow
    try:
        # Fetch token manually for httpx usage
        token = api_client.configuration.get_access_token()
        
        endpoint = f"{base_url}/api/v2/flows/execute"
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "Accept": "application/json"
        }
        
        async with httpx.AsyncClient(timeout=60.0) as client:
            response = await client.post(
                endpoint,
                headers=headers,
                json=payload
            )
            
            if response.status_code == 200:
                result = response.json()
                print(f"Flow Execution ID: {result.get('id', 'N/A')}")
                
                # Extract data based on flow output configuration
                if "output" in result:
                    print("Customer Data Found:")
                    print(result["output"])
                    return result["output"]
                else:
                    print("Flow completed but no output data returned.")
                    return {}
                    
            elif response.status_code == 401:
                print("Authentication failed. Check Client ID/Secret.")
                sys.exit(1)
            elif response.status_code == 403:
                print("Permission denied. Ensure 'flow:execute' scope is granted.")
                sys.exit(1)
            elif response.status_code == 429:
                print("Rate limited. Retry after delay.")
                sys.exit(1)
            else:
                print(f"Unexpected status code: {response.status_code}")
                print(response.text)
                return {}

    except ApiException as e:
        print(f"SDK API Exception: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"Unexpected error: {e}")
        sys.exit(1)

# --- Entry Point ---

async def main():
    # Configuration
    config = init_genesys_config()
    
    # Inputs
    FLOW_ID = os.getenv("TARGET_FLOW_ID")
    TEST_PHONE = "+15550199999" # E.164 format
    
    if not FLOW_ID:
        print("Error: TARGET_FLOW_ID environment variable not set.")
        sys.exit(1)
        
    print(f"Looking up customer for phone: {TEST_PHONE} using Flow: {FLOW_ID}")
    
    # Execute
    customer_data = await lookup_customer_by_phone(config, FLOW_ID, TEST_PHONE)
    
    if customer_data:
        print("\nLookup Successful.")
    else:
        print("\nLookup completed, but no specific customer data was returned in the standard output format.")

if __name__ == "__main__":
    asyncio.run(main())

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token is invalid, expired, or the Client ID/Secret is incorrect.
  • Fix: Verify that GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET are correct. Ensure the client has the flow:execute scope. The SDK handles refresh, but if the initial grant fails, check the credentials.

Error: 403 Forbidden

  • Cause: The OAuth client does not have the required permissions.
  • Fix: Go to the Genesys Cloud Admin Console > Security > OAuth Clients. Edit the client and ensure the following scopes are checked:
    • flow:execute
    • architect:flow:execute
    • If the flow uses external data sources, ensure the client has access to those sources if they are restricted.

Error: 400 Bad Request (Flow Not Found)

  • Cause: The flowId in the payload is invalid or the flow is not published.
  • Fix: Verify the TARGET_FLOW_ID is a valid UUID. Ensure the Architect Flow is in “Published” status. Unpublished flows cannot be executed via API.

Error: Flow Timeout

  • Cause: The GetExternalContact action is taking too long to resolve, or the external data source is slow.
  • Fix: Increase the timeout value in the payload (max 30000 ms for synchronous execution). If the lookup is inherently slow, consider making the flow asynchronous and polling the execution status using GET /api/v2/flows/executions/{id}.

Error: Empty Output

  • Cause: The flow executed successfully, but the GetExternalContact action did not find a match, or the flow did not configure a return value.
  • Fix:
    1. Check the Architect Flow. Ensure the GetExternalContact action has a “No Match” path. If no match is found, the flow might end without returning data.
    2. Ensure the flow ends with a “Return” action that outputs the variable containing the contact data.

Official References