How to configure the Web Messaging deployment to customize colors and launcher position

How to configure the Web Messaging deployment to customize colors and launcher position

What You Will Build

  • You will build a script that retrieves an existing Web Messaging deployment, modifies its visual theme (launcher button color, background color) and layout position, and saves the changes back to Genesys Cloud.
  • This tutorial uses the Genesys Cloud Pure Cloud Platform API v2 (/api/v2/webchat/deployments) and the Python SDK.
  • The implementation is written in Python 3.9+ using httpx for HTTP requests and the official genesyscloud SDK for object mapping.

Prerequisites

  • OAuth Client Type: A Genesys Cloud OAuth client with private or public type.
  • Required Scopes:
    • webchat:deployment:read (to retrieve the deployment)
    • webchat:deployment:write (to update the deployment)
    • webchat:deployment:theme:read and webchat:deployment:theme:write (optional, depending on theme complexity, but usually covered by deployment write)
  • SDK Version: genesyscloud Python SDK v2.20.0 or later.
  • Runtime: Python 3.9+
  • Dependencies:
    pip install genesyscloud httpx
    

Authentication Setup

Genesys Cloud uses OAuth 2.0 for authentication. For server-to-server integrations (like this configuration script), the Client Credentials Grant flow is the standard approach. This flow requires your client ID, client secret, and the environment URL.

The following function handles the token acquisition. In a production environment, you should cache this token and implement refresh logic, as tokens expire after 3600 seconds (1 hour). For this tutorial, we request a new token each run to ensure simplicity and reliability.

import httpx
import json
from typing import Optional

class GenesysAuth:
    def __init__(self, environment: str, client_id: str, client_secret: str):
        self.environment = environment
        self.client_id = client_id
        self.client_secret = client_secret
        self.token_url = f"https://{environment}.mypurecloud.com/oauth/token"
        self.access_token: Optional[str] = None
        self.expires_at: Optional[float] = None

    def get_access_token(self) -> str:
        """
        Retrieves an OAuth2 access token using Client Credentials Grant.
        """
        # In production, check if self.access_token is valid and not expired
        # before making a new request.
        
        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }
        
        data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": "webchat:deployment:read webchat:deployment:write"
        }

        try:
            with httpx.Client() as client:
                response = client.post(
                    self.token_url,
                    headers=headers,
                    data=data,
                    timeout=10.0
                )
                response.raise_for_status()
                token_data = response.json()
                self.access_token = token_data["access_token"]
                return self.access_token
        except httpx.HTTPStatusError as e:
            raise Exception(f"Authentication failed: {e.response.status_code} - {e.response.text}")
        except Exception as e:
            raise Exception(f"Unexpected error during authentication: {str(e)}")

Implementation

Step 1: Retrieve the Existing Deployment Configuration

Before modifying colors or positions, you must retrieve the current state of the Web Messaging deployment. The deployment ID is required for all subsequent operations. You can find this ID in the Admin UI under Channels > Web Messaging, or by listing all deployments via the API.

The endpoint GET /api/v2/webchat/deployments/{deploymentId} returns the full deployment object, including the theme and launcher configurations.

import httpx
import os

class WebMessagingConfigurator:
    def __init__(self, auth: GenesysAuth, environment: str):
        self.auth = auth
        self.base_url = f"https://{environment}.mypurecloud.com/api/v2"
        self.headers = {
            "Authorization": f"Bearer {auth.get_access_token()}",
            "Content-Type": "application/json",
            "Accept": "application/json"
        }

    def get_deployment(self, deployment_id: str) -> dict:
        """
        Retrieves the full configuration of a specific Web Messaging deployment.
        """
        endpoint = f"{self.base_url}/webchat/deployments/{deployment_id}"
        
        try:
            with httpx.Client() as client:
                response = client.get(endpoint, headers=self.headers, timeout=10.0)
                
                if response.status_code == 401:
                    # Token may have expired; refresh and retry once
                    self.headers["Authorization"] = f"Bearer {self.auth.get_access_token()}"
                    response = client.get(endpoint, headers=self.headers, timeout=10.0)
                    
                response.raise_for_status()
                return response.json()
                
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise Exception(f"Deployment with ID {deployment_id} not found.")
            elif e.response.status_code == 429:
                raise Exception("Rate limit exceeded. Please wait and retry.")
            else:
                raise Exception(f"HTTP Error: {e.response.status_code} - {e.response.text}")
        except Exception as e:
            raise Exception(f"Failed to fetch deployment: {str(e)}")

Step 2: Modify Theme Colors and Launcher Position

The Web Messaging deployment object contains two critical sections for visual customization:

  1. theme: Controls colors, fonts, and bubble styling.
  2. launcher: Controls the position, icon, and behavior of the chat entry point.

Understanding the Theme Structure

The theme object is nested. Key properties for color customization include:

  • theme.bubble.primaryColor: The main background color of the chat window header and buttons.
  • theme.bubble.secondaryColor: Accent colors for hover states or secondary actions.
  • theme.text.primaryColor: Text color inside the chat window.
  • theme.icon.primaryColor: Color of the launcher icon.

Understanding the Launcher Position

The launcher object contains:

  • launcher.position: A string value, typically "bottom-right" or "bottom-left".
  • launcher.type: Usually "button" or "icon".
  • launcher.offsetX and launcher.offsetY: Pixel offsets from the edge of the screen.

In this step, we define a function that takes the retrieved deployment JSON and mutates it with new values.

def customize_deployment_theme(
    deployment: dict, 
    primary_color: str, 
    launcher_position: str = "bottom-right",
    offset_x: int = 20,
    offset_y: int = 20
) -> dict:
    """
    Mutates the deployment dictionary to apply custom colors and position.
    
    Args:
        deployment: The JSON object retrieved from the API.
        primary_color: Hex code for the primary brand color (e.g., "#007BFF").
        launcher_position: "bottom-right" or "bottom-left".
        offset_x: Horizontal distance from the edge in pixels.
        offset_y: Vertical distance from the edge in pixels.
        
    Returns:
        The modified deployment dictionary.
    """
    
    # Ensure theme structure exists
    if "theme" not in deployment:
        deployment["theme"] = {}
    
    # Ensure bubble theme exists
    if "bubble" not in deployment["theme"]:
        deployment["theme"]["bubble"] = {}
        
    # Apply Colors
    # Note: Genesys Cloud expects hex strings with '#' prefix
    deployment["theme"]["bubble"]["primaryColor"] = primary_color
    
    # Optional: Set a contrasting secondary color if not already defined
    if "secondaryColor" not in deployment["theme"]["bubble"]:
        # Simple logic to invert brightness slightly for contrast if needed
        # In production, you might pass this as a parameter
        deployment["theme"]["bubble"]["secondaryColor"] = "#FFFFFF" 

    # Apply Launcher Position
    if "launcher" not in deployment:
        deployment["launcher"] = {}
        
    deployment["launcher"]["position"] = launcher_position
    deployment["launcher"]["offsetX"] = offset_x
    deployment["launcher"]["offsetY"] = offset_y
    
    # Optional: Ensure the launcher icon color contrasts with the background
    # If the primary color is dark, set icon to white, otherwise black
    # This is a simple heuristic; robust apps should calculate luminance
    if primary_color.startswith("#"):
        hex_color = primary_color.lstrip("#")
        r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
        luminance = 0.299 * r + 0.587 * g + 0.114 * b
        icon_color = "#FFFFFF" if luminance < 128 else "#000000"
        
        if "icon" not in deployment["theme"]:
            deployment["theme"]["icon"] = {}
        deployment["theme"]["icon"]["primaryColor"] = icon_color

    return deployment

Step 3: Save the Updated Deployment

Once the JSON object is modified, you must send it back to Genesys Cloud using a PUT request. The PUT method replaces the entire deployment configuration. This means you must send the full object returned from Step 1, not just the changed fields.

Critical Note: The id field must be preserved. The etag field is optional for PUT but recommended for concurrency control. If you omit etag, Genesys Cloud will overwrite any concurrent changes made by an admin in the UI.

    def update_deployment(self, deployment_id: str, deployment_config: dict) -> dict:
        """
        Updates the Web Messaging deployment with the new configuration.
        """
        endpoint = f"{self.base_url}/webchat/deployments/{deployment_id}"
        
        # Remove etag if present to force overwrite, or keep it for optimistic locking
        # For this tutorial, we force overwrite to ensure the script works
        if "etag" in deployment_config:
            del deployment_config["etag"]

        try:
            with httpx.Client() as client:
                response = client.put(
                    endpoint, 
                    headers=self.headers, 
                    json=deployment_config, 
                    timeout=10.0
                )
                
                if response.status_code == 401:
                    self.headers["Authorization"] = f"Bearer {self.auth.get_access_token()}"
                    response = client.put(
                        endpoint, 
                        headers=self.headers, 
                        json=deployment_config, 
                        timeout=10.0
                    )
                    
                response.raise_for_status()
                return response.json()
                
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 400:
                raise Exception(f"Bad Request: Validation failed. Check the JSON payload structure. {e.response.text}")
            elif e.response.status_code == 403:
                raise Exception("Forbidden: Your OAuth client lacks webchat:deployment:write scope.")
            elif e.response.status_code == 429:
                raise Exception("Rate limit exceeded. Please implement exponential backoff.")
            else:
                raise Exception(f"HTTP Error: {e.response.status_code} - {e.response.text}")
        except Exception as e:
            raise Exception(f"Failed to update deployment: {str(e)}")

Complete Working Example

The following script combines all components. It authenticates, retrieves a deployment by ID, customizes the primary color to a specific brand blue (#1E90FF) and moves the launcher to the bottom-left corner, then saves the changes.

import httpx
import json
import sys
from typing import Optional

# --- Authentication Class ---
class GenesysAuth:
    def __init__(self, environment: str, client_id: str, client_secret: str):
        self.environment = environment
        self.client_id = client_id
        self.client_secret = client_secret
        self.token_url = f"https://{environment}.mypurecloud.com/oauth/token"
        self.access_token: Optional[str] = None

    def get_access_token(self) -> str:
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": "webchat:deployment:read webchat:deployment:write"
        }
        try:
            with httpx.Client() as client:
                response = client.post(self.token_url, headers=headers, data=data, timeout=10.0)
                response.raise_for_status()
                self.access_token = response.json()["access_token"]
                return self.access_token
        except httpx.HTTPStatusError as e:
            raise Exception(f"Authentication failed: {e.response.status_code} - {e.response.text}")

# --- Configuration Class ---
class WebMessagingConfigurator:
    def __init__(self, auth: GenesysAuth, environment: str):
        self.auth = auth
        self.base_url = f"https://{environment}.mypurecloud.com/api/v2"
        self.headers = {
            "Authorization": f"Bearer {auth.get_access_token()}",
            "Content-Type": "application/json",
            "Accept": "application/json"
        }

    def get_deployment(self, deployment_id: str) -> dict:
        endpoint = f"{self.base_url}/webchat/deployments/{deployment_id}"
        try:
            with httpx.Client() as client:
                response = client.get(endpoint, headers=self.headers, timeout=10.0)
                if response.status_code == 401:
                    self.headers["Authorization"] = f"Bearer {self.auth.get_access_token()}"
                    response = client.get(endpoint, headers=self.headers, timeout=10.0)
                response.raise_for_status()
                return response.json()
        except httpx.HTTPStatusError as e:
            raise Exception(f"Fetch failed: {e.response.status_code} - {e.response.text}")

    def update_deployment(self, deployment_id: str, deployment_config: dict) -> dict:
        endpoint = f"{self.base_url}/webchat/deployments/{deployment_id}"
        if "etag" in deployment_config:
            del deployment_config["etag"]
        try:
            with httpx.Client() as client:
                response = client.put(endpoint, headers=self.headers, json=deployment_config, timeout=10.0)
                if response.status_code == 401:
                    self.headers["Authorization"] = f"Bearer {self.auth.get_access_token()}"
                    response = client.put(endpoint, headers=self.headers, json=deployment_config, timeout=10.0)
                response.raise_for_status()
                return response.json()
        except httpx.HTTPStatusError as e:
            raise Exception(f"Update failed: {e.response.status_code} - {e.response.text}")

def customize_deployment_theme(deployment: dict, primary_color: str, launcher_position: str) -> dict:
    if "theme" not in deployment:
        deployment["theme"] = {}
    if "bubble" not in deployment["theme"]:
        deployment["theme"]["bubble"] = {}
        
    deployment["theme"]["bubble"]["primaryColor"] = primary_color
    
    if "launcher" not in deployment:
        deployment["launcher"] = {}
    deployment["launcher"]["position"] = launcher_position
    
    # Set icon color based on primary color luminance
    if primary_color.startswith("#"):
        hex_color = primary_color.lstrip("#")
        r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
        luminance = 0.299 * r + 0.587 * g + 0.114 * b
        icon_color = "#FFFFFF" if luminance < 128 else "#000000"
        
        if "icon" not in deployment["theme"]:
            deployment["theme"]["icon"] = {}
        deployment["theme"]["icon"]["primaryColor"] = icon_color
        
    return deployment

def main():
    # Configuration
    ENVIRONMENT = "us-east-1" # Change to your environment (e.g., eu-west-1, au)
    CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
    CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")
    DEPLOYMENT_ID = os.getenv("GENESYS_DEPLOYMENT_ID")

    if not all([CLIENT_ID, CLIENT_SECRET, DEPLOYMENT_ID]):
        print("Error: Missing environment variables. Set GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET, and GENESYS_DEPLOYMENT_ID.")
        sys.exit(1)

    try:
        # 1. Authenticate
        print("Authenticating...")
        auth = GenesysAuth(ENVIRONMENT, CLIENT_ID, CLIENT_SECRET)
        token = auth.get_access_token()
        print("Authentication successful.")

        # 2. Initialize Configurator
        configurator = WebMessagingConfigurator(auth, ENVIRONMENT)

        # 3. Retrieve Deployment
        print(f"Retrieving deployment {DEPLOYMENT_ID}...")
        deployment = configurator.get_deployment(DEPLOYMENT_ID)
        print("Deployment retrieved.")

        # 4. Customize
        # Set primary color to Dodger Blue and position to bottom-left
        NEW_PRIMARY_COLOR = "#1E90FF"
        NEW_POSITION = "bottom-left"
        
        print(f"Applying customizations: Color={NEW_PRIMARY_COLOR}, Position={NEW_POSITION}")
        modified_deployment = customize_deployment_theme(deployment, NEW_PRIMARY_COLOR, NEW_POSITION)

        # 5. Update Deployment
        print("Saving changes to Genesys Cloud...")
        updated_deployment = configurator.update_deployment(DEPLOYMENT_ID, modified_deployment)
        
        print("Success! Deployment updated.")
        print(f"New Primary Color: {updated_deployment['theme']['bubble']['primaryColor']}")
        print(f"New Launcher Position: {updated_deployment['launcher']['position']}")

    except Exception as e:
        print(f"Error: {str(e)}")
        sys.exit(1)

if __name__ == "__main__":
    import os
    main()

Common Errors & Debugging

Error: 400 Bad Request - Validation Failed

Cause: The JSON payload sent to the PUT endpoint contains invalid structure or missing required fields. Web Messaging deployments have a complex nested schema. If you manually constructed the JSON instead of retrieving and modifying it, you likely missed a required field like deploymentId or routingType.
Fix: Always retrieve the existing deployment using GET, modify the dictionary in memory, and send the full object back. Do not strip out fields you do not intend to change.

Error: 403 Forbidden - Scope Denied

Cause: The OAuth client used for authentication does not have the webchat:deployment:write scope.
Fix: Go to Admin > Security > OAuth Clients, select your client, and ensure webchat:deployment:write is checked. Save the client and generate a new token.

Error: 409 Conflict - ETag Mismatch

Cause: You included the etag from the GET response in your PUT request, but another admin modified the deployment in the UI between your GET and PUT calls. Genesys Cloud uses ETags for optimistic locking.
Fix: For automated configuration scripts, it is often safer to delete the etag field from the payload before sending the PUT request, as shown in the update_deployment method above. This forces the server to accept your version as the latest. If you require strict concurrency control, catch the 409 error, re-fetch the deployment, re-apply your changes, and retry.

Error: 429 Too Many Requests

Cause: You exceeded the Genesys Cloud API rate limits. This is common if you are looping through many deployments.
Fix: Implement exponential backoff. Wait 1 second, then 2, then 4, etc., before retrying the request. The Retry-After header in the response may indicate how long to wait.

Official References