Skip to main content
IBM watsonx Orchestrate’s Embedded Chat feature allows you to integrate the watsonx Orchestrate chat experience into your own web UI applications. To ensure secure communication between your application and the watsonx Orchestrate service, the Embedded Chat feature includes security mechanisms that use public-key cryptography.

Generating embedded web chat

To simplify integration with your website, the CLI includes the orchestrate channels webchat embed command. This command takes the name of an agent and produces a script tag that you can place in the <head></head> section of your page for the currently active environment.
BASH
orchestrate channels webchat embed --agent-name=test_agent1
When targeting the local environment, the command uses your agent’s draft variant. On a production instance, it defaults to using your agent’s live (deployed) variant. The following is an example of the command’s output:
OUTPUT
<script>
  window.wxOConfiguration = {
    orchestrationID: "your-orgID_orchestrationID", // Adds control over chat display mode (e.g., fullscreen)
    hostURL: "https://dl.watson-orchestrate.ibm.com", // or region-specific host
    rootElementID: "root",
    showLauncher: false,
    deploymentPlatform: "ibmcloud", // Required for IBM Cloud embed, can be skipped for other embed scenarios
    crn: "your-org-crn", // Required for IBM Cloud embed, can be skipped for other embed scenarios. For more information, see [FAQs](https://www.ibm.com/docs/en/watsonx/watson-orchestrate/base?topic=experience-faqs#how-can-i-know-my-crn-id)
    chatOptions: {
      agentId: "your-agent-id",
      agentEnvironmentId: "your-agent-env-id",
    },
    layout: {
      form: "fullscreen-overlay", // Options: float | custom | fullscreen-overlay - if not specified float is default
      showOrchestrateHeader: true, // Optional: shows top agent header bar
      width: "600px", // Optional: honored when form is float only
      height: "600px", // Optional: honored when form is float only
    },
  };

  setTimeout(function () {
    const script = document.createElement("script");
    script.src = `${window.wxOConfiguration.hostURL}/wxochat/wxoLoader.js?embed=true`;
    script.addEventListener("load", function () {
      wxoLoader.init();
    });
    document.head.appendChild(script);
  }, 0);
</script>

Embedded chat security

By default, security is enabled, but not configured for the embedded chat. This means:
  • The embedded chat will not function until security is properly configured
  • You must configure both IBM and client key pairs for the chat to work
  • Alternatively, you can explicitly disable security to allow anonymous access

Security Architecture

The embedded chat uses RSA public-key cryptography to secure communication. The configuration involves two key pairs:
  1. IBM Key Pair
    • Generated by: watsonx Orchestrate service.
    • Public key: Shared with your application. Your application uses this key to encrypt the user_payload section of the JWT sent to watsonx Orchestrate.
    • Private key: Stored securely by watsonx Orchestrate.cUsed to decrypt the user_payload section of the JWT.
  2. Client Key Pair
    • Generated by: You (or a security configuration tool).
    • Public key: Shared with watsonx Orchestrate. Used to verify that JWTs originate from your application.
    • Private key: Remains with you and must be stored securely. Used to sign JWTs sent to watsonx Orchestrate.
  3. JWT Authentication
    • When security is enabled, your application must:
      • Generate a JWT signed with your private key (Client Key Pair).
      • Include the JWT in all requests to the Embedded Chat API. watsonx Orchestrate validates the token using your public key.
When security is enabled:
  • All requests to the Embedded Chat API must include a valid JWT token
  • The token must be signed with your private key
  • The watsonx Orchestrate service validates the token using your public key
  • This prevents unauthorized access to your watsonx Orchestrate instance
When security is disabled:
  • Requests to the Embedded Chat API do not require authentication
  • Anyone with access to your web application where chat is Embedded can access your watsonx Orchestrate instance. In addition, your Watson Orchestrate instance allows anonymous authentication to a limited set of Apis, which is required to get your embed chat to work for anonymous users.
  • This option should only be used for specific use cases where anonymous chat access is required.
  • Ensure your watsonx Orchestrate instance in this case, does not provide access to sensitive data or access to tools configured with functional credentials that access sensitive data.

Enabling security

1

Prerequisites

  • IBM watsonx Orchestrate instance
  • API Key with administrative privileges
  • Service Instance URL from your watsonx Orchestrate instance
  • On macOS and Linux: OpenSSL installed on your system (for key generation)
  • Python Installed on your system (for key extraction from APIs)
2

Get the `wxO-embed-chat-security-tool.sh`

Copy the following automated script to configure security:
wxO-embed-chat-security-tool.sh
#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# IBM watsonx Orchestrate — Embedded Chat Security Helper (v4)
# Single Bash script for IBM Cloud / AWS / CPD.
#
# WHAT THIS DOES
#  - Turns Embedded Chat security ON or OFF for a single instance.
#  - When turning security ON:
#      • Uses your CLIENT PUBLIC key (existing or newly generated).
#      • Asks watsonx Orchestrate for an IBM PUBLIC key.
#      • Saves the keys locally in ./keys and updates the instance config.
#  - When turning security OFF:
#      • Disables embedded chat security for that instance (anonymous access).
#  - When viewing:
#      • Only fetches and prints the current configuration (no changes).
#
# IMPORTANT:
#  - By default, Embedded Chat security is ON but not configured.
#    In that state, Embedded Chat will NOT work until keys are set,
#    or security is explicitly disabled for anonymous access.
#  - Fully configured security requires BOTH:
#      • An IBM PUBLIC key generated by watsonx Orchestrate.
#      • Your CLIENT PUBLIC key (from your own key pair).
#
# SUPPORTED INSTANCE URL FORMATS
#  IBM Cloud / AWS:
#    https://api.<host>/instances/<INSTANCE_ID>
#
#  CPD (Cloud Pak for Data):
#    https://<host>/orchestrate/<CPD_INSTANCE>/instances/<INSTANCE_ID>
#
# ARGUMENTS (optional, in order)
#   1) WXO_API_KEY            (IBM Cloud / AWS only; ignored for CPD)
#   2) FULL_INSTANCE_API_URL  (Service instance URL as above)
#
# ENVIRONMENT VARIABLES (optional)
#   ACTION=enable|disable|view    (single-shot, no main menu)
#   IAM_URL=<explicit IAM base>   (Override IAM URL for non-CPD flows)
#   WXO_API_KEY                   (Same as arg 1, if not given)
#   FULL_INSTANCE_API_URL         (Same as arg 2, if not given)
#   API_URL_WITH_INSTANCE         (Legacy name for FULL_INSTANCE_API_URL)
#   CPD_USERNAME / CPD_PASSWORD   (For CPD login, otherwise prompted)
#
# CPD TOKEN:
#   POST https://<host>/icp4d-api/v1/authorize { username, password }
#
# NON-CPD TOKEN:
#   POST ${IAM_URL}/siusermgr/api/1.0/apikeys/token { apikey }
# ============================================================

# ===============================
# Colors / formatting (best effort)
# ===============================
if [[ -t 1 ]]; then
  BOLD='\033[1m'
  GREEN='\033[0;32m'
  YELLOW='\033[0;33m'
  RED='\033[0;31m'
  BLUE='\033[0;34m'
  NC='\033[0m' # No Color
else
  BOLD='' GREEN='' YELLOW='' RED='' BLUE='' NC=''
fi

# Keep original ACTION for non-interactive single-shot runs
NON_INTERACTIVE_ACTION="${ACTION:-}"

# ===============================
# Helpers
# ===============================

banner() {
  echo "============================================================"
  echo -e "${BOLD}$1${NC}"
  echo "============================================================"
}

need_cmd() {
  if ! command -v "$1" >/dev/null 2>&1; then
    echo -e "${RED}❌ Required command '$1' not found. Please install it and retry.${NC}"
    exit 1
  fi
}

prompt_if_empty() {
  local __varname="$1"
  local __prompt="$2"
  if [[ -z "${!__varname:-}" ]]; then
    read -rp "$__prompt" "$__varname"
    export "$__varname"
  fi
}

prompt_secret_if_empty() {
  local __varname="$1"
  local __prompt="$2"
  if [[ -z "${!__varname:-}" ]]; then
    read -rs -p "$__prompt" "$__varname"
    echo
    export "$__varname"
  fi
}

# Returns base URL with any leading 'api.' stripped from the host.
strip_leading_api() {
  local in_url="$1"
  if [[ "${in_url}" =~ ^(https?://)([^/]+)(/.*)?$ ]]; then
    local scheme="${BASH_REMATCH[1]}"
    local host="${BASH_REMATCH[2]}"
    host="${host#api.}"
    echo "${scheme}${host}"
    return 0
  fi
  echo "${in_url}"
  return 0
}

# Get scheme://host from a full URL
origin_from_url() {
  local in_url="$1"
  if [[ "${in_url}" =~ ^(https?://[^/]+) ]]; then
    echo "${BASH_REMATCH[1]}"
    return 0
  fi
  echo "${in_url}"
  return 0
}

# Read a PEM file and return JSON string (jq -Rs .), normalize line endings
read_pem_as_json_string() {
  local pem_path="$1"

  if [[ ! -f "$pem_path" ]]; then
    echo -e "${RED}❌ PEM file not found: $pem_path${NC}" >&2
    return 1
  fi
  if [[ ! -r "$pem_path" ]]; then
    echo -e "${RED}❌ PEM file not readable: $pem_path${NC}" >&2
    return 1
  fi

  if grep -qE -- '-----BEGIN (RSA )?PUBLIC KEY-----' "$pem_path"; then
    : # public key — good
  elif grep -qE -- '-----BEGIN (RSA )?PRIVATE KEY-----' "$pem_path"; then
    echo -e "${YELLOW}⚠️  You provided a PRIVATE key PEM. The API expects the PUBLIC key. Continuing…${NC}" >&2
  else
    echo -e "${YELLOW}⚠️  File lacks standard PUBLIC/PRIVATE KEY PEM header. Continuing…${NC}" >&2
  fi

  tr -d '\r' < "$pem_path" | sed '${/^$/d;}' | jq -Rs .
}

# Quick help for instance URL
show_instance_help() {
  echo
  echo -e "${BOLD}How to find your Service instance URL:${NC}"
  echo "1. Open watsonx Orchestrate in your browser."
  echo "2. Click the profile icon in the top-right corner."
  echo "3. Choose \"Settings\"\"API details\"."
  echo "4. Copy the \"Service instance URL\", which looks like:"
  echo -e "   ${BLUE}https://api.us-south.watson-orchestrate.ibm.com/instances/20250807-1007-4445-5049-459a42144389${NC}"
  echo
  echo "Paste that full URL into this script when prompted."
  echo
}

# Print a human-friendly summary of the /config payload
print_config_summary() {
  local config_json="$1"

  # Detect whether is_security_enabled field exists, and preserve true/false
  local is_enabled
  is_enabled="$(
    echo "${config_json}" \
      | jq -r 'if has("is_security_enabled") then (.is_security_enabled|tostring) else "__missing__" end' 2>/dev/null \
      || echo "__missing__"
  )"

  # Extract keys
  local public_key client_public_key
  public_key="$(echo "${config_json}" | jq -r '.public_key // ""')"
  client_public_key="$(echo "${config_json}" | jq -r '.client_public_key // ""')"

  local has_client="false"
  local has_ibm="false"
  [[ -n "${client_public_key}" ]] && has_client="true"
  [[ -n "${public_key}" ]] && has_ibm="true"

  if [[ "${is_enabled}" == "__missing__" || -z "${is_enabled}" ]]; then
    echo -e "${YELLOW}Could not find is_security_enabled in the response. Raw config is shown above.${NC}"
    return 0
  fi

  # =========================
  # SECURITY DISABLED STATES
  # =========================
  if [[ "${is_enabled}" == "false" ]]; then
    echo -e "Embedded chat security is: ${YELLOW}DISABLED${NC}"

    if [[ "${has_client}" == "true" && "${has_ibm}" == "false" ]]; then
      echo "Security is disabled, but a client public key is still configured."
      echo "This may be a stale or incomplete setup."
      echo
      echo "To clean this up, you can:"
      echo "  • Replace it with the correct client public key, and when you turn security back ON,"
      echo "    generate a new IBM public key; or"
      echo "  • Clear the client public key if you intend to keep anonymous access."
      echo
      echo "When you enable security again, use the IBM public key to encrypt the user_payload"
      echo "in your identity token, and sign the token with the private key that matches your"
      echo "client public key."
      echo
      echo "Disabled security will allow anonymous users to access your embedded chat."
      echo "Anyone who can access your website will be able to open the chat without authentication."
      echo
      echo "Only use disabled security if your use case explicitly requires anonymous access and your"
      echo "watsonx Orchestrate instance does not expose sensitive data or tools configured with"
      echo "functional credentials."
    else
      echo "Anyone who can access your website can open the embedded chat without authentication."
      echo
      echo "Only use disabled security when your use case explicitly requires anonymous access and your"
      echo "watsonx Orchestrate instance does not expose sensitive data or tools configured with"
      echo "functional credentials."
    fi
    return 0
  fi

  # If we got here and it's not "true", just show raw
  if [[ "${is_enabled}" != "true" ]]; then
    echo -e "${YELLOW}Could not interpret is_security_enabled=\"${is_enabled}\". Raw config is shown above.${NC}"
    return 0
  fi

  # =========================
  # SECURITY ENABLED STATES
  # =========================
  echo -e "Embedded chat security is: ${GREEN}ENABLED${NC}"

  if [[ "${has_client}" == "true" && "${has_ibm}" == "true" ]]; then
    # Fully configured: security ON + both keys present
    echo -e "${GREEN}Embedded chat security is enabled and configured.${NC}"
    echo "Security is enabled and both IBM and client key pairs are configured."
    echo
    echo "Update your embed script to pass a valid RS256-signed JWT using the private key"
    echo "that matches your client public key, so watsonx Orchestrate can authenticate users"
    echo "and allow embedded chat to work as expected."

  elif [[ "${has_client}" == "true" && "${has_ibm}" == "false" ]]; then
    # Security ON + client key present + IBM key missing
    echo -e "${GREEN}Embedded chat security is enabled (encryption not configured).${NC}"
    echo "A client public key is configured, but Encrypt sensitive information is not set up."
    echo
    echo "Generate an IBM public key so you can encrypt the user_payload in your identity token"
    echo "and fully enable encrypted embedded chat security."

  elif [[ "${has_client}" == "false" && "${has_ibm}" == "true" ]]; then
    # Security ON + IBM key present + client key missing
    echo -e "${YELLOW}Embedded chat security is enabled but the client public key is missing.${NC}"
    echo "A client public key is required to validate your identity tokens."
    echo
    echo "Paste a valid client public key in the Chat user identity section and update"
    echo "your configuration so watsonx Orchestrate can verify your RS256-signed JWTs."

  elif [[ "${has_client}" == "false" && "${has_ibm}" == "false" ]]; then
    # Security ON + no keys at all
    echo -e "${YELLOW}Embedded chat security is enabled but not fully configured.${NC}"
    echo "Security is currently turned on, but embedded chat will not work until both key"
    echo "pairs are configured:"
    echo "  • An IBM public key generated by watsonx Orchestrate."
    echo "  • Your application’s client public key used to verify signed tokens."
    echo
    echo "To complete configuration, we recommend the following steps:"
    echo "  1) Provide a valid client public key in the Chat user identity section."
    echo "  2) Generate an IBM public key for Encrypt sensitive information."
    echo "  3) Update your embed script to use the IBM public key to encrypt user_payload."
  else
    # Catch-all for any other unusual combination
    echo -e "${YELLOW}Security is enabled, but the key configuration is incomplete or unusual.${NC}"
    echo "Check that both of the following are set correctly:"
    echo "  • IBM public key (for encrypting user_payload)."
    echo "  • Client public key (for verifying your signed JWTs)."
  fi
}

# ===============================
# Dependencies
# ===============================
need_cmd curl
need_cmd jq
need_cmd sed
need_cmd openssl
need_cmd tr
need_cmd grep

# ===============================
# Welcome + optional help
# ===============================
banner "IBM watsonx Orchestrate — Embedded Chat Security Helper (v4)"
echo "This helper manages embedded chat security for a single watsonx Orchestrate instance."
echo
echo "By default, embedded chat security is ON but not configured."
echo "In that state, embedded chat will not work until:"
echo "  • both IBM and client key pairs are configured, or"
echo "  • security is explicitly turned OFF to allow anonymous access."
echo

read -rp "Do you need help finding your Service instance URL? (y/N): " NEED_HELP
NEED_HELP="${NEED_HELP:-n}"
if [[ "${NEED_HELP}" == [Yy] ]]; then
  show_instance_help
fi

# ===============================
# 1) Inputs (URL first), then creds
# ===============================
WXO_API_KEY="${1:-${WXO_API_KEY:-}}"
FULL_INSTANCE_API_URL="${2:-${FULL_INSTANCE_API_URL:-${API_URL_WITH_INSTANCE:-}}}"

if [[ -z "${FULL_INSTANCE_API_URL}" ]]; then
  banner "watsonx Orchestrate Embedded Chat Security"
  echo "Please provide the Service instance URL from the API details page:"
  echo "  • IBM Cloud / AWS:"
  echo "      https://api.<host>/instances/<INSTANCE_ID>"
  echo "  • CPD:"
  echo "      https://<host>/orchestrate/<CPD_INSTANCE>/instances/<INSTANCE_ID>"
  echo
fi

prompt_if_empty FULL_INSTANCE_API_URL "Enter Service instance URL: "

# ===============================
# 2) Parse URL, detect CPD vs Non-CPD
# ===============================
IS_CPD="false"
API_ORIGIN="$(origin_from_url "${FULL_INSTANCE_API_URL}")"

# CPD pattern
if [[ "${FULL_INSTANCE_API_URL}" =~ ^(https?://[^/]+)/orchestrate/([^/]+)/instances/([^/?#]+) ]]; then
  IS_CPD="true"
  API_URL_BASE="${BASH_REMATCH[1]}/orchestrate/${BASH_REMATCH[2]}"
  CPD_INSTANCE_NAME="${BASH_REMATCH[2]}"
  WXO_INSTANCE_ID="${BASH_REMATCH[3]}"
# Non-CPD pattern
elif [[ "${FULL_INSTANCE_API_URL}" =~ ^(https?://[^/]+)/instances/([^/?#]+) ]]; then
  IS_CPD="false"
  API_URL_BASE="${BASH_REMATCH[1]}"
  WXO_INSTANCE_ID="${BASH_REMATCH[2]}"
else
  echo -e "${RED}❌ Invalid Service instance URL.${NC}"
  echo "   IBM Cloud / AWS example:"
  echo "     https://api.us-south.watson-orchestrate.ibm.com/instances/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
  echo "   CPD example:"
  echo "     https://cpd-cpd-instance-1.apps.example.com/orchestrate/cpd-instance-1/instances/1757991870301390"
  echo "Got: ${FULL_INSTANCE_API_URL}"
  exit 1
fi

# Heuristic: also mark CPD if host contains 'cpd' (defensive)
if [[ "${API_ORIGIN}" == *"cpd"* ]]; then
  IS_CPD="true"
fi

banner "Inputs"
echo "WXO_INSTANCE_ID:   ${WXO_INSTANCE_ID}"
echo "API_URL_BASE:      ${API_URL_BASE}"
echo "API_ORIGIN:        ${API_ORIGIN}"
echo "Detected Platform: $([[ "${IS_CPD}" == "true" ]] && echo "CPD" || echo "IBM Cloud / AWS")"
echo

# ===============================
# 3) Token acquisition (once)
# ===============================
if [[ "${IS_CPD}" == "true" ]]; then
  # CPD: username + password -> token at <origin>/icp4d-api/v1/authorize
  banner "Fetching CPD token"
  CPD_USERNAME="${CPD_USERNAME:-}"
  CPD_PASSWORD="${CPD_PASSWORD:-}"
  prompt_if_empty CPD_USERNAME "Enter CPD Username: "
  prompt_secret_if_empty CPD_PASSWORD "Enter CPD Password (hidden): "

  WXO_TOKEN="$(
    curl --fail -sS \
      --request POST \
      --url "${API_ORIGIN}/icp4d-api/v1/authorize" \
      --header 'Content-Type: application/json' \
      --header 'cache-control: no-cache' \
      --data "{\"username\":\"${CPD_USERNAME}\",\"password\":\"${CPD_PASSWORD}\"}" --insecure \
    | jq -r .token
  )"

  if [[ -z "${WXO_TOKEN}" || "${WXO_TOKEN}" == "null" ]]; then
    echo -e "${RED}❌ Failed to fetch CPD token. Check your credentials and host.${NC}"
    exit 1
  fi
  echo -e "${GREEN}✅ CPD token acquired.${NC}"
  echo

else
  # IBM Cloud / AWS: API key -> IAM token selection
  if [[ -z "${WXO_API_KEY}" ]]; then
    echo "IBM Cloud / AWS flow selected."
    echo "You will be prompted for WXO_API_KEY (input hidden)."
  fi

  prompt_secret_if_empty WXO_API_KEY "Enter WXO_API_KEY (hidden): "

  # Auto-select IAM_URL unless provided
  if [[ -z "${IAM_URL:-}" ]]; then
    API_URL_NOAPI="$(strip_leading_api "${API_URL_BASE}")"
    case "${API_URL_NOAPI}" in
      # IBM Cloud - PROD
      https://au-syd.watson-orchestrate.cloud.ibm.com|\
      https://jp-tok.watson-orchestrate.cloud.ibm.com|\
      https://eu-de.watson-orchestrate.cloud.ibm.com|\
      https://eu-gb.watson-orchestrate.cloud.ibm.com|\
      https://us-south.watson-orchestrate.cloud.ibm.com|\
      https://us-east.watson-orchestrate.cloud.ibm.com|\
      https://ca-tor.watson-orchestrate.cloud.ibm.com)
        IAM_URL="https://iam.platform.saas.ibm.com"
        ;;
      # AWS - PROD
      https://ap-southeast-1.dl.watson-orchestrate.ibm.com|\
      https://ap-south-1.dl.watson-orchestrate.ibm.com|\
      https://eu-central-1.dl.watson-orchestrate.ibm.com|\
      https://dl.watson-orchestrate.ibm.com|\
      https://cio.watson-orchestrate.ibm.com)
        IAM_URL="https://iam.platform.saas.ibm.com"
        ;;
      # GovCloud (mapped to PROD IAM)
      https://origin-us-gov-east-1.watson-orchestrate.prep.ibmforusgov.com)
        IAM_URL="https://iam.platform.saas.ibm.com"
        ;;
      # STAGING / TEST
      https://us-south.watson-orchestrate.test.cloud.ibm.com|\
      https://perfus-south.watson-orchestrate.test.cloud.ibm.com|\
      https://staging-wa.watson-orchestrate.ibm.com|\
      https://wodlperf.watson-orchestrate.ibm.com|\
      https://preprod.dl.watson-orchestrate.ibm.com)
        IAM_URL="https://iam.platform.test.saas.ibm.com"
        ;;
      # DEV
      https://us-south.watson-orchestrate-dev.test.cloud.ibm.com|\
      https://dev-wa.watson-orchestrate.ibm.com|\
      https://dev-fedtest.watson-orchestrate.ibm.com)
        IAM_URL="https://iam.platform.dev.saas.ibm.com"
        ;;
      *)
        echo -e "${YELLOW}⚠️  Unknown API host after normalization: ${API_URL_NOAPI}${NC}"
        echo "   Falling back to PROD IAM."
        IAM_URL="https://iam.platform.saas.ibm.com"
        ;;
    esac

    export IAM_URL
    banner "IAM URL selected"
    echo "Normalized API host: ${API_URL_NOAPI}"
    echo "IAM_URL:             ${IAM_URL}"
    echo
  else
    banner "IAM URL selected (from environment)"
    echo "IAM_URL: ${IAM_URL}"
    echo
  fi

  banner "Fetching IAM token"
  WXO_TOKEN="$(
    curl --fail -sS \
      --request POST \
      --url "${IAM_URL}/siusermgr/api/1.0/apikeys/token" \
      --header 'accept: application/json' \
      --header 'content-type: application/json' \
      --data "{\"apikey\": \"${WXO_API_KEY}\"}" \
    | jq -r .token
  )"

  if [[ -z "${WXO_TOKEN}" || "${WXO_TOKEN}" == "null" ]]; then
    echo -e "${RED}❌ Failed to fetch IAM token. Check your WXO_API_KEY and IAM_URL.${NC}"
    exit 1
  fi
  echo -e "${GREEN}✅ Token acquired.${NC}"
  echo
fi

# ===============================
# 4) Endpoints (CPD vs Non-CPD)
# ===============================
if [[ "${IS_CPD}" == "true" ]]; then
  CONFIG_ENDPOINT="${API_URL_BASE}/instances/${WXO_INSTANCE_ID}/v1/embed/secure/config"
  GENKEY_ENDPOINT="${API_URL_BASE}/instances/${WXO_INSTANCE_ID}/v1/embed/secure/generate-key-pair"
else
  CONFIG_ENDPOINT="${API_URL_BASE}/instances/${WXO_INSTANCE_ID}/v1/embed/secure/config"
  GENKEY_ENDPOINT="${API_URL_BASE}/instances/${WXO_INSTANCE_ID}/v1/embed/secure/generate-key-pair"
fi

# ===============================
# 5) Common action runner (enable/disable/view + verify)
# ===============================
run_action() {
  local action="$1"

  if [[ "${action}" == "enable" ]]; then
    banner "Turn ON embedded chat security and configure keys"

    mkdir -p keys

    echo "Choose how to provide the client public key:"
    echo "  1) Use an existing PUBLIC key PEM (path on disk)"
    echo "  2) Generate a NEW RSA 4096 key pair now"
    read -rp "Enter 1 or 2: " key_choice

    local CLIENT_PUBLIC_KEY_JSON=""
    local KEY_CHOICE="${key_choice}"

    case "$key_choice" in
      1)
        read -rp "Enter path to client PUBLIC key PEM (e.g., /path/to/jwtRS256.key.pub): " CLIENT_PUB_PEM_PATH
        CLIENT_PUBLIC_KEY_JSON="$(read_pem_as_json_string "$CLIENT_PUB_PEM_PATH")"
        ;;
      2)
        echo "Generating client RSA key pair in ./keys ..."
        openssl genrsa -out keys/example-jwtRS256.key 4096 >/dev/null 2>&1
        openssl rsa -in keys/example-jwtRS256.key -pubout -out keys/example-jwtRS256.key.pub >/dev/null 2>&1
        CLIENT_PUBLIC_KEY_JSON="$(read_pem_as_json_string "keys/example-jwtRS256.key.pub")"
        echo "🔐 Generated client key files:"
        echo "  • keys/example-jwtRS256.key        (client PRIVATE key — keep this secure)"
        echo "  • keys/example-jwtRS256.key.pub    (client PUBLIC key)"
        ;;
      *)
        echo -e "${RED}❌ Invalid choice.${NC}"
        return 1
        ;;
    esac

    if [[ -z "${CLIENT_PUBLIC_KEY_JSON:-}" || "${CLIENT_PUBLIC_KEY_JSON}" == "null" ]]; then
      echo -e "${RED}❌ Could not prepare client PUBLIC key for payload.${NC}"
      return 1
    fi

    echo "Requesting IBM public key from watsonx Orchestrate..."
    curl --fail -sS \
      --request POST \
      --url "${GENKEY_ENDPOINT}" \
      --header "Authorization: Bearer ${WXO_TOKEN}" --insecure \
    | jq -r '.public_key' > keys/ibmPublic.key.pub

    if [[ ! -s keys/ibmPublic.key.pub ]]; then
      echo -e "${RED}❌ Failed to retrieve IBM public key.${NC}"
      return 1
    fi

    local IBM_PUBLIC_KEY_JSON
    IBM_PUBLIC_KEY_JSON="$(read_pem_as_json_string "keys/ibmPublic.key.pub")"

    local CONFIG_PAYLOAD
    CONFIG_PAYLOAD=$(
      cat <<JSON
{
  "public_key": ${IBM_PUBLIC_KEY_JSON},
  "client_public_key": ${CLIENT_PUBLIC_KEY_JSON},
  "is_security_enabled": true
}
JSON
    )

    echo "Updating embedded chat security configuration..."
    local RESULT
    RESULT="$(
      curl --fail -sS \
        --request POST \
        --url "${CONFIG_ENDPOINT}" \
        --header "Authorization: Bearer ${WXO_TOKEN}" \
        --header 'Content-Type: application/json' \
        --data "${CONFIG_PAYLOAD}" --insecure
    )"

    echo -e "${GREEN}✅ Embedded chat security has been turned ON and keys are configured (server response below).${NC}"
    echo "Server response:"
    echo "${RESULT}" | jq .
    echo

    echo "Saved IBM PUBLIC key to: keys/ibmPublic.key.pub"
    if [[ "${KEY_CHOICE}" == "1" ]]; then
      echo "Use this IBM public key to encrypt the user_payload in your identity token."
    else
      echo "Use:"
      echo "  • keys/example-jwtRS256.key        (client PRIVATE key for signing JWTs)"
      echo "  • keys/example-jwtRS256.key.pub    (client PUBLIC key configured on the instance)"
      echo "  • keys/ibmPublic.key.pub           (IBM PUBLIC key for encrypting user_payload)"
    fi
    echo

  elif [[ "${action}" == "disable" ]]; then
    # For interactive runs (no NON_INTERACTIVE_ACTION), ask for confirmation
    if [[ -z "${NON_INTERACTIVE_ACTION}" ]]; then
      echo
      echo -e "${RED}Disable embedded chat security?${NC}"
      echo "Disabling security will allow anonymous users to access your embedded chat."
      echo "Anyone who can access your website will be able to open the chat without authentication."
      echo
      echo "Only disable security if your use case explicitly requires anonymous access and your"
      echo "watsonx Orchestrate instance does not expose sensitive data or tools configured with"
      echo "functional credentials."
      echo
      read -rp "Type 'yes' to confirm disabling security: " CONFIRM_DISABLE
      if [[ "${CONFIRM_DISABLE}" != "yes" ]]; then
        echo "Operation cancelled. Security settings were not changed."
        return 0
      fi
    fi

    banner "Turn OFF embedded chat security (anonymous access)"

    local DISABLE_PAYLOAD
    DISABLE_PAYLOAD='{"is_security_enabled": false}'

    echo "Updating embedded chat security configuration..."
    local RESULT
    RESULT="$(
      curl --fail -sS \
        --request POST \
        --url "${CONFIG_ENDPOINT}" \
        --header "Authorization: Bearer ${WXO_TOKEN}" \
        --header 'Content-Type: application/json' \
        --data "${DISABLE_PAYLOAD}" --insecure
    )"

    echo -e "${YELLOW}✅ Embedded chat security has been turned OFF for this instance.${NC}"
    echo "Server response:"
    echo "${RESULT}" | jq .
    echo

  else
    banner "View current embedded chat security configuration (read-only)"
  fi

  # Common: GET /config and summary
  banner "Check current configuration"
  local VERIFY
  VERIFY="$(
    curl --fail -sS \
      --request GET \
      --url "${CONFIG_ENDPOINT}" \
      --header "Authorization: Bearer ${WXO_TOKEN}" \
      --header 'Accept: application/json' --insecure
  )"
  echo "Raw configuration from the service:"
  echo "${VERIFY}" | jq .
  echo
  echo "Summary:"
  print_config_summary "${VERIFY}"
  echo
  echo -e "${GREEN}Action completed.${NC}"
}

# ===============================
# 6) Non-interactive (ACTION set) or interactive main menu loop
# ===============================
if [[ -n "${NON_INTERACTIVE_ACTION}" ]]; then
  # Single-shot mode for automation: just run the given action and exit.
  run_action "${NON_INTERACTIVE_ACTION}"
  exit 0
fi

# Interactive main menu loop
while true; do
  echo
  echo "What would you like to do for this instance?"
  echo "  1) Turn ON embedded chat security and configure keys (recommended)"
  echo "  2) Turn OFF embedded chat security and allow anonymous access"
  echo "  3) View current embedded chat security configuration (no changes)"
  echo "  4) Exit"
  read -rp "Enter 1, 2, 3 or 4: " choice

  case "$choice" in
    1)
      run_action "enable"
      ;;
    2)
      run_action "disable"
      ;;
    3)
      run_action "view"
      ;;
    4)
      echo "Exiting."
      break
      ;;
    *)
      echo -e "${RED}❌ Invalid choice. Please enter 1, 2, 3 or 4.${NC}"
      ;;
  esac
done
3

Change the script's permissions

On Unix-based systems (macOS and Linux), change the permissions to run the script:
chmod +x wxO-embed-chat-security-tool.sh
4

Run the script

Run the script and follow the instructions to enable or disable security:
./wxO-embed-chat-security-tool.sh
After you configure security:
  1. The tool generates an IBM key pair via the API
  2. The tool generates a client key pair using OpenSSL
  3. Both public keys are configured in the service
  4. Security is enabled
All keys are saved in the wxo_security_config directory:
  • ibm_public_key.pem: IBM’s public key in PEM format
  • ibm_public_key.txt: IBM’s public key in single-line format
  • client_private_key.pem: Your private key (keep it secure!)
  • client_public_key.pem: Your public key in PEM format
  • client_public_key.txt: Your public key in single-line format

Context variables for embedded web chat

To use context variables in embedded web chat, include them inside the JWT token. You can add context variables to a JWT token using a JavaScript script. The following script shows how to include context variables inside a JWT token:
createJWT
const fs = require('fs');
const RSA = require('node-rsa');
const crypto = require('crypto');
const jwtLib = require('jsonwebtoken');
const express = require('express');
const path = require('path');
const { v4: uuid } = require('uuid');

const router = express.Router();

// This is your private key that you will keep on your server. This is used to sign the jwt. You will paste your public
// key into the appropriate field on the Security tab of the web chat settings page.
// This public key is used to validate the signature on the jwt.
const PRIVATE_KEY = fs.readFileSync(path.join(__dirname, '../keys/private.key'));

//The code below will use this key to encrypt the user payload inside the JWT.
const PUBLIC_KEY = fs.readFileSync(path.join(__dirname, '../keys/public.pub'));


// A time period of 45 days in milliseconds.
const TIME_45_DAYS = 1000 * 60 * 60 * 24 * 45;

/**
 * Generates a signed JWT. The JWT used here will always be assigned a user ID using the given anonymous user ID. If
 * the user is authenticated and we have session info, then info about the user will also be added to the JWT.
 * Always use the anonymous user ID even if the user is authenticated because changing the user ID in the middle of
 * a session is not allowed.
 */
function createJWTString(anonymousUserID, sessionInfo,context) {
  // This is the content of the JWT. You would normally look up the user information from a user profile.
  const jwtContent = {
    // This is the subject of the JWT which will be the ID of the user.
    //
    // This user ID will be available under integrations.channel.private.user.id in dialog and
    // system_integrations.channel.private.user.id in actions.
    sub: anonymousUserID,
    // This object is optional and contains any data you wish to include as part of the JWT. This data will be
    // encrypted using the public key so it will not be visible to your users.
    user_payload: {
      custom_message: 'Encrypted message',
      name: 'Anonymous',
    },
    context
  };

  // If the user is authenticated, then add the user's real info to the JWT.
  if (sessionInfo) {
    jwtContent.user_payload.name = sessionInfo.userName;
    jwtContent.user_payload.custom_user_id = sessionInfo.customUserID;
  }

  const dataString = JSON.stringify(jwtContent.user_payload);

  // Encrypt the data
  const encryptedBuffer = crypto.publicEncrypt(
    {
      key: PUBLIC_KEY,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
      oaepHash: 'sha256'  // Specify OAEP padding with SHA256
    },
    Buffer.from(dataString, 'utf-8')
  );

  // Convert encrypted data to base64
  jwtContent.user_payload = encryptedBuffer.toString('base64');
  console.log(jwtContent.user_payload)


  // Now sign the jwt content to make the actual jwt. We are giving this a very short expiration time (10 seconds)
  // to demonstrate the web chat capability of fetching a new token when it expires. In a production environment,
  // you would likely want to set this to a much higher value or leave it out entirely.
  const jwtString = jwtLib.sign(jwtContent, PRIVATE_KEY, {
    algorithm: 'RS256',
    expiresIn: '10000000s',
  });

  return jwtString;
}

/**
 * Gets or sets the anonymous user ID cookie. This will also ensure that an existing cookie is updated with a new 45
 * day expiration time.
 */
function getOrSetAnonymousID(request, response) {
  let anonymousID = request.cookies['ANONYMOUS-USER-ID'];
  if (!anonymousID) {
    // If we don't already have an anonymous user ID, then create one. Normally you would want to use a full UUID,
    // but for the sake of this example we are going to shorten it to just five characters to make them easier to read.
    anonymousID = `anon-${uuid().substr(0, 5)}`;
  }

  // Here we set the value of the cookie and give it an expiration date of 45 days. We do this even if we already
  // have an ID to make sure that we update the expiration date to a new 45 days.
  response.cookie('ANONYMOUS-USER-ID', anonymousID, {
    expires: new Date(Date.now() + TIME_45_DAYS),
    httpOnly: true,
  });

  return anonymousID;
}

/**
 * Returns the session info for an authenticated user.
 */
function getSessionInfo(request) {
  // Normally the cookie would contain a session token that we would use to look up the user's info from something
  // like a database. But for the sake of simplicity in this example the session cookie directly contains the user's
  // info.
  const sessionInfo = request.cookies.SESSION_INFO;
  if (sessionInfo) {
    return JSON.parse(sessionInfo);
  }
  return null;
}

/**
 * Handles the createJWT request.
 */
function createJWT(request, response) {
  const anonymousUserID = getOrSetAnonymousID(request, response);
  const sessionInfo = getSessionInfo(request);

  const context = {
    dev_id: 23424,
    dev_name: "Name",
    is_active: true
  }

  response.send(createJWTString(anonymousUserID, sessionInfo,context));
}

router.get('/', createJWT);

module.exports = router;
After generating the JWT token, pass it to the embedded web chat. The following example shows how to do that:
JavaScript
<script>
    function getUserId() {
        let embed_user_id = sessionStorage.embed_user_id;
        if (!embed_user_id) {
            embed_user_id = Math.trunc(Math.random() * 1000000);
            sessionStorage.embed_user_id = embed_user_id;
        }
        return embed_user_id;
    }
    function preSendHandler(event) {
        if (event?.message?.content) {
            event.message.content = event.message.content.toUpperCase();
        }
    }

    function sendHandler(event) {
        console.log('send event', event);
    }

    function feedbackHandler(event) {
        console.log('feedback', event);
    }

    function preReceiveHandler(event) {
        event?.content?.map((element) => {
            element.type = 'date';
        });
    }

    function receiveHandler(event) {
        console.log('received event', event);
    }

    function userDefinedResponseHandler(event) {
        console.log('userDefinedResponse event', event);
        event.hostElement.innerHTML = `
                    <cds-code-snippet>
                        node -v Lorem ipsum dolor sit amet, consectetur adipisicing elit. Blanditiis,
                        veritatis voluptate id incidunt molestiae officia possimus, quasi itaque
                        alias, architecto hic, dicta fugit? Debitis delectus quidem explicabo vitae
                        laboriosam!
                    </cds-code-snippet>
                    <br><br>
                    <div style="background-color:orange;color:white;padding:10px;">
                        <p>${event.contentItem?.template || '[No message content]'}</p>
                    </div>`;
    }

    function onChatLoad(instance) {
        instance.on('chatstarted', (instance) => {
            window.wxoChatInstance = instance;
        });
        instance.on('pre:send', preSendHandler);
        instance.on('send', sendHandler);
        instance.on('pre:receive', preReceiveHandler);
        instance.on('receive', receiveHandler);
        instance.on('feedback', feedbackHandler);
        instance.on('userDefinedResponse', userDefinedResponseHandler);
    }
    async function getIdentityToken() {
        // This will make a call to your server to request a new JWT.
        const result = await fetch(
            "http://localhost:3003/createJWT?user_id=" + getUserId()
        );
        window.wxOConfiguration.token = await result.text();
    }
    window.wxOConfiguration = {
        orchestrationID: "20250430-0912-2925-309a-35c6bef54760_20250430-0949-0287-00f2-33dc583100c9",
        hostURL: "https://dl.watson-orchestrate.ibm.com",
        rootElementID: "root",
        chatOptions: {
            agentId: "852431a8-32dd-4925-8cc3-9ea3d3162726",
            agentEnvironmentId: "5d769a04-9445-4768-a687-710d6e9a24cf",
        },
        style: {
            headerColor: '#b8b890',
            userMessageBackgroundColor: '#ffa31a',
            primaryColor: '#33ff3c',
        },
        showLauncher: false,
        layout: {
            form: 'fullscreen-overlay',
            showOrchestrateHeader: true,
        }
    };
    setTimeout(function () {
        const script = document.createElement('script');
        script.src = `${window.wxOConfiguration.hostURL}/wxochat/wxoLoader.js?embed=true`;
        script.addEventListener('load', function () {
            wxoLoader.init();
        });
        document.head.appendChild(script);
    }, 0);
    getIdentityToken().then(() => {
        const script = document.createElement("script");
        script.src = `${window.wxOConfiguration.hostUrl}/wxochat/wxoLoader.js?embed=true`;
        script.addEventListener("load", function () {
            wxoLoader.init();
        });
        document.head.appendChild(script);
    });
</script>

Customizing embedded web chat

Configuring header

Header is an optional property in WXOConfiguration that controls whether header actions appear.
ParameterTypeDescription
header.showResetButtonbooleanDisplays the Reset Chat button in the header when set to true. Default is true.
header.showAiDisclaimebooleanDisplays the AI disclaimer icon/button in the header when set to true. Default is true.

Customizing styles

You can customize embedded web chats to create a unique chat interface that better fits your webpage. To apply custom styles, add a style component inside the window.wxOConfiguration object in your web chat script. In this component, you can configure the following elements:
ParameterTypeDescription
headerColorstringSet a six-digit hex code that defines the chat header color.
userMessageBackgroundColorstringSet a six-digit hex code that defines the user message bubble color.
primaryColorstringSet a six-digit hex code that defines the interactive elements color.
style.showBackgroundGradientbooleanDisplays the background gradient when set to true. Default is true.
The following is an example of how to customize the embedded web chat using the style component inside window.wxOConfiguration:
JavaScript
<script>
window.wxOConfiguration = {
  orchestrationID: "my-tenant-id",
  hostURL: "my-host-url",
  rootElementID: "root",
  showLauncher: false,
  chatOptions: {
    agentId: "test_agent1",
    agentEnvironmentId: "my-agent-draft-env-id"
  },
  
 style: {
   headerColor: '#000000',
   userMessageBackgroundColor: '#000000',
   primaryColor: '#000000'
  },
  
};

setTimeout(function() {
  const script = document.createElement('script');
  script.src = `${window.wxOConfiguration.hostURL}/wxochat/wxoLoader.js?embed=true`;
  script.addEventListener('load', () => wxoLoader.init());
  document.head.appendChild(script);
}, 0);
</script>

Customizing layout

The watsonx Orchestrate embed supports a flexible layout object to control how and where the chat UI appears.
ParameterTypeDefaultDescription
rootElementIDstring(fullscreen-overlay only) ID of the container node to mount chat into.
showLauncherbooleantrue(fullscreen-overlay only) Show the bubble launcher (true) or render chat immediately (false).
layout.formstringfloatDefines the layout form of your web chat.

Use fullscreen-overlay to display the web chat in fullscreen mode. No additional parameters are required.

Use float to display the web chat as a floating window. Also configure:
  • width: Width of the web chat
  • height: Height of the web chat
Use custom to define a custom layout. Also configure the customElement parameter with your custom element.
layout.widthstring(float only) Popup width (e.g. ‘350px’, ‘30rem’).
layout.heightstring(float only) Popup height (e.g. ‘500px’, ‘40rem’).
layout.showOrchestrateHeaderbooleantrueRender the standard header bar (true) or hide it(false).
layout.customElementHTMLElementelement reference to render into.
JavaScript
<script>
window.wxOConfiguration = {
  orchestrationID: "my-tenant-id",
  hostURL: "my-host-url",
  rootElementID: "root",            // fullscreen-overlay only
  showLauncher: false,              // fullscreen-overlay only, false = direct render, true = launcher bubble

  chatOptions: {
    agentId: "12345_test_agent1",            // required
    agentEnvironmentId: "my-agent-env-id"    // required
  },

  layout: {
    form: 'float',                           // 'fullscreen-overlay' | 'float' | 'custom'
    width: '600px',                          // float only
    height: '600px',                         // float only
    showOrchestrateHeader: true,            // hide header if false
    customElement: hostElement              // custom only
  }
};

setTimeout(function() {
  const script = document.createElement('script');
  script.src = `${window.wxOConfiguration.hostURL}/wxochat/wxoLoader.js?embed=true`;
  script.addEventListener('load', () => wxoLoader.init());
  document.head.appendChild(script);
}, 0);
</script>
The following is an example of how to customize the layout of the embedded web chat to display it in fullscreen mode:
JavaScript
<script>
window.wxOConfiguration = {
  orchestrationID: "my-tenant-id",
  hostURL: "my-host-url",
  rootElementID: "root",
  showLauncher: false,
  chatOptions: {
    agentId: "test_agent1",
    agentEnvironmentId: "my-agent-draft-env-id"
  },
  
  layout:{
    form: 'fullscreen-overlay',
    showOrchestrateHeader: true,
  }
  
};

setTimeout(function() {
  const script = document.createElement('script');
  script.src = `${window.wxOConfiguration.hostURL}/wxochat/wxoLoader.js?embed=true`;
  script.addEventListener('load', () => wxoLoader.init());
  document.head.appendChild(script);
}, 0);
</script>

Enabling thumbs-up and thumbs-down

In the embedded chat, you need to manually enable thumbs-up and thumbs-down feedback using pre:receive handlers. First, subscribe to the pre:receive event to inject feedback options. Then, handle submitted feedback through the feedback event. The following script shows how to configure feedback in the embedded chat:
JavaScript
<script>
function feedbackHandler(event) {
    console.log('feedback', event);
}

function preReceiveHandler(event) {
    console.log('pre-receive event', event);


    const lastItem = event?.content?.[event.content.length - 1];
    if (lastItem) {
        lastItem.message_options = {
            feedback: {
                is_on: true,
                show_positive_details: false,
                show_negative_details: true,
                // Note, these positive details are not used as long as show_positive_details is false.
                positive_options: {
                    categories: ['Funny', 'Helpful', 'Correct'],
                    disclaimer: "Provide content that can be shared publicly.",
                },
                negative_options: {
                    categories: ['Inaccurate', 'Incomplete', 'Too long', 'Irrelevant', 'Other'],
                    disclaimer: "Provide content that can be shared publicly.",
                },
            },
        };
    }
}



function onChatLoad(instance) {

    instance.on('pre:receive', preReceiveHandler);
    instance.on('feedback', feedbackHandler);
}

window.wxOConfiguration = {
    ...
        };
setTimeout(function () {
            ...
        }, 0);
</script>

Events reference

Embedded web chat supports a variety of events that allow you to trigger specific actions or customize behavior. The following tables list all supported events, grouped by category.

Customization Events

Event nameDescription
userDefinedResponseTriggered when a response contains an unrecognized or user_defined response type.

Message Events

Event nameDescription
pre:sendTriggered before the web chat sends a message to the assistant.
sendTriggered after the web chat sends a message to the assistant.
pre:receiveTriggered before the web chat receives a response from the assistant.
receiveTriggered after the web chat receives a response from the assistant.
pre:restartConversationTriggered before the conversation restarts. Useful for alerting the user that the chat will reset, allowing them to complete any ongoing actions (e.g., finishing a tool call).
restartConversationTriggered after the conversation restarts, before a new session begins. Useful for displaying specific UI elements when a new session starts.

View Events

Event nameDescription
view:pre:changeTriggered before the view state changes.
view:changeTriggered after the view state changes.
pre:threadLoadedTriggered when a user navigates to a chat thread in full-screen embedded chat. Useful for displaying custom responses or UI elements.

Security Events

Event nameDescription
identityTokenExpiredTriggered when security is enabled and the JWT token expires.
authTokenNeededTriggered when the embedded chat requires a refreshed or updated authentication token.

Miscellaneous Events

Event nameDescription
chat:readyTriggered when the web chat is fully loaded and ready to receive user input. Useful for displaying a welcome message or initializing UI components.

Events example

The following example shows how to configure events in the embedded web chat:
JavaScript
    <script>
        function preSendHandler(event, instance) {
            console.log('pre:send event', event);
            if (event?.message?.message?.content) {
                event.message.message.content = event.message.message?.content.toUpperCase();
            }
        }

        function sendHandler(event, instance) {
            console.log('send event', event);
        }

        function feedbackHandler(event, instance) {
            console.log('feedback', event);
        }

        function preReceiveHandler(event, instance) {
            console.log('pre-receive event', event);
            event?.message?.content?.map((element) => {
                if (element?.text?.includes('assistant')) {
                    element.text = element.text.replace('assistant', 'Agent');
                }
                element.type = 'user_defined';
            });

            const lastItem = event?.message?.content?.[event.message?.content.length - 1];
            if (lastItem) {
                lastItem.message_options = {
                    feedback: {
                        is_on: true,
                        show_positive_details: false,
                        show_negative_details: true,

                        positive_options: {
                            categories: ['Funny', 'Helpful', 'Correct'],
                            disclaimer: "Provide content that can be shared publicly.",
                        },
                        negative_options: {
                            categories: ['Inaccurate', 'Incomplete', 'Too long', 'Irrelevant', 'Other'],
                            disclaimer: "Provide content that can be shared publicly.",
                        },
                    },
                };
            }
        }

        function receiveHandler(event, instance) {
            console.log('received event', event);
            instance.off('pre:receive', preReceiveHandler);
            instance.updateAuthToken("wrong-or-expired-token")
        }

        function userDefinedResponseHandler(event, instance) {
            console.log('userDefinedResponse event', event);
            event.hostElement.innerHTML = `
                    <cds-code-snippet>
                        node -v Lorem ipsum dolor sit amet, consectetur adipisicing elit. Blanditiis,
                        veritatis voluptate id incidunt molestiae officia possimus, quasi itaque
                        alias, architecto hic, dicta fugit? Debitis delectus quidem explicabo vitae
                        laboriosam!
                    </cds-code-snippet>
                    <br><br>
                    <div style="background-color:orange;color:white;padding:10px;">
                        <p>${event.contentItem?.text || '[No message content]'}</p>
                    </div>`;
        }

        function preRestartConversationHandler(event, instance) {
            console.log('pre:restartConversation event', event);
        }

        let calledRestartConversation = false;
        function restartConversationHandler(event, instance) {
            console.log('restartConversationHandler event', event);
            if (!calledRestartConversation) {
                setTimeout(() => {
                    instance.send('Hello from embedded webchat second time')
                }, 3000);
                calledRestartConversation = true;
            }

        }

        function preThreadLoadedHandler(event, instance) {
            console.log('pre:threadLoaded event', event);
            event.messages[0].content[0].text = 'Modified prompt in thread history'
        }

        async function authTokenNeededHandler(event, instance) {
            console.log('authTokenNeeded event', event);
            event.authToken = "<Refreshed Token>"
        }

        function onChatLoad(instance) {
            instance.on('chat:ready', (event, instance) => {
                console.log('chat:ready', event);
            });
            instance.once('pre:send', preSendHandler);
            instance.on('send', sendHandler);
            instance.once('pre:receive', preReceiveHandler);
            instance.on('receive', receiveHandler);
            instance.on('feedback', feedbackHandler);
            instance.on('userDefinedResponse', userDefinedResponseHandler);
            instance.on('pre:restartConversation', preRestartConversationHandler);
            instance.on('restartConversation', restartConversationHandler);
            instance.on('pre:threadLoaded', preThreadLoadedHandler);
            instance.on('authTokenNeeded', authTokenNeededHandler);
        }
        window.wxOConfiguration = {
            clientVersion: 'latest',
            orchestrationID: '<tenantId>',
            hostUrl: 'http://localhost:3000',
            showLauncher: true,
            rootElementId: 'root',
            chatOptions: {
                agentId: '<agentId>',
                agentEnvironmentId: '<agentEnvironmentId>',
                onLoad: onChatLoad,
            },
        };
        setTimeout(function () {
            const script = document.createElement('script');
            script.src = `${window.wxOConfiguration.hostUrl}/wxoLoader.js?embed=true`;
            script.addEventListener('load', function () {
                wxoLoader.init();
            });
            document.head.appendChild(script);
        }, 0);
    </script>