How to Build a Custom Chat UI Using the WebSocket-Based Guest API

How to Build a Custom Chat UI Using the WebSocket-Based Guest API

What You Will Build

  • You will build a custom, low-latency web chat interface that connects directly to Genesys Cloud via the WebSocket Guest API, bypassing the standard Web Messaging Widget SDK.
  • This implementation uses the native WebSocket protocol for real-time bidirectional communication and the REST API for session initialization.
  • The tutorial covers JavaScript (ES6+) with vanilla DOM manipulation to ensure the logic is portable to any frontend framework.

Prerequisites

  • OAuth Client: A Genesys Cloud OAuth Client configured with the webmessaging:guest scope. You must enable the “Web Messaging” capability in your Genesys Cloud organization settings.
  • API Version: Genesys Cloud API v2.
  • Runtime: Any modern web browser with WebSocket support (Chrome, Firefox, Edge, Safari).
  • Dependencies: None. This tutorial uses native fetch and WebSocket APIs. No external libraries like Socket.io or Axios are required, though you may use them in production.
  • Environment: You need a valid client_id and a mechanism to obtain a guest_token (either via a backend proxy or client-side PKCE flow if supported by your security policy).

Authentication Setup

The WebSocket Guest API does not use standard OAuth Bearer tokens for the WebSocket connection itself. Instead, it uses a short-lived guest_token obtained via the REST API. This token acts as the handshake credential for the WebSocket upgrade.

The flow is:

  1. Call the REST endpoint to create a guest session.
  2. Receive the guest_token and session_id.
  3. Use the guest_token in the WebSocket query parameters to establish the connection.

Step 1: Obtain the Guest Token

You must authenticate with Genesys Cloud to get an OAuth access token first, then use that to create the guest session. For this tutorial, we assume you have a backend endpoint that returns the guest_token directly, or you are using a client-side flow where you already have an access_token with the webmessaging:guest scope.

Here is the code to create the guest session using the REST API.

/**
 * Creates a new guest session and returns the guest token.
 * @param {string} accessToken - The OAuth access token with webmessaging:guest scope.
 * @param {string} organizationId - Your Genesys Cloud Organization ID.
 * @returns {Promise<Object>} - Object containing guest_token and session_id.
 */
async function createGuestSession(accessToken, organizationId) {
    const url = `https://api.us-east-1.mygen.com/api/v2/webmessaging/guest/sessions`;
    
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${accessToken}`,
        'X-Genesys-Organization': organizationId // Required for multi-tenant environments
    };

    const payload = {
        "guestName": "Dev Advocate Demo",
        "guestEmail": "demo@genesys.com",
        "attributes": {
            "customAttributeKey": "customAttributeValue"
        }
    };

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: headers,
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`Failed to create guest session: ${response.status} ${errorText}`);
        }

        const data = await response.json();
        return {
            guestToken: data.guest_token,
            sessionId: data.session_id,
            organizationId: data.organization_id
        };
    } catch (error) {
        console.error("Error creating guest session:", error);
        throw error;
    }
}

OAuth Scope Required: webmessaging:guest

Expected Response:

{
  "guest_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "organization_id": "gen-12345678-90ab-cdef-1234-567890abcdef",
  "expires_in": 86400
}

Implementation

Step 2: Establish the WebSocket Connection

Once you have the guest_token, you can open a WebSocket connection. The Genesys Cloud WebSocket endpoint is distinct from the REST API endpoint.

Endpoint Format:
wss://webmessaging.us-east-1.mygen.com/api/v2/webmessaging/guest/ws?guest_token={GUEST_TOKEN}&organization_id={ORG_ID}

Note: Replace us-east-1 with your specific region (e.g., eu-west-1, ap-southeast-2).

/**
 * Establishes a WebSocket connection to Genesys Cloud.
 * @param {Object} sessionInfo - Object containing guestToken, organizationId, and sessionId.
 * @param {Function} onMessage - Callback for incoming messages.
 * @param {Function} onError - Callback for connection errors.
 * @returns {WebSocket} - The active WebSocket instance.
 */
function connectWebSocket(sessionInfo, onMessage, onError) {
    const { guestToken, organizationId } = sessionInfo;
    const region = 'us-east-1'; // Change this to match your Genesys Cloud region
    const wsUrl = `wss://webmessaging.${region}.mygen.com/api/v2/webmessaging/guest/ws?guest_token=${guestToken}&organization_id=${organizationId}`;

    const ws = new WebSocket(wsUrl);

    ws.onopen = () => {
        console.log("WebSocket connection established.");
        // Send a 'ping' to keep the connection alive if required by your infrastructure
        // ws.send(JSON.stringify({ type: 'ping' }));
    };

    ws.onmessage = (event) => {
        try {
            const message = JSON.parse(event.data);
            onMessage(message);
        } catch (e) {
            console.error("Failed to parse WebSocket message:", e);
        }
    };

    ws.onerror = (error) => {
        console.error("WebSocket error:", error);
        onError(error);
    };

    ws.onclose = (event) => {
        console.log(`WebSocket closed with code: ${event.code}, reason: ${event.reason}`);
        // Implement reconnection logic here if necessary
    };

    return ws;
}

Step 3: Routing Messages and Sending Data

The WebSocket stream carries various message types. You must filter these based on the type field in the JSON payload. Common types include:

  • agent_typing: Agent is typing.
  • agent_message: Agent sent a message.
  • guest_message_delivered: Confirmation that your message was received.
  • routing: Status updates about queue position or agent assignment.

Sending a Message

To send a message to the agent, you must send a JSON object with the type set to message and include the session_id.

/**
 * Sends a chat message to the agent via WebSocket.
 * @param {WebSocket} ws - The active WebSocket connection.
 * @param {string} sessionId - The session ID from the guest creation step.
 * @param {string} text - The message text.
 */
function sendMessage(ws, sessionId, text) {
    if (ws.readyState !== WebSocket.OPEN) {
        console.error("WebSocket is not open.");
        return;
    }

    const messagePayload = {
        "type": "message",
        "session_id": sessionId,
        "text": text
    };

    ws.send(JSON.stringify(messagePayload));
    
    // Optimistically add to UI
    addMessageToUI("user", text);
}

Processing Incoming Messages

You need a router to handle different message types.

function handleIncomingMessage(message) {
    switch (message.type) {
        case 'agent_message':
            addMessageToUI("agent", message.text);
            break;
        case 'agent_typing':
            showTypingIndicator(true);
            break;
        case 'agent_not_typing':
            showTypingIndicator(false);
            break;
        case 'routing':
            updateRoutingStatus(message);
            break;
        case 'guest_message_delivered':
            // Optional: Update message status to 'sent'
            break;
        case 'error':
            console.error("Genesys Cloud Error:", message.reason);
            alert(`Error: ${message.reason}`);
            break;
        default:
            console.warn("Unknown message type:", message.type, message);
    }
}

Step 4: Handling Reconnection and Heartbeats

WebSockets can drop due to network instability. The Genesys Cloud Guest API supports a simple ping/pong mechanism, but it is often better to rely on the browser’s WebSocket lifecycle events and implement an exponential backoff reconnection strategy.

let wsInstance = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const BASE_DELAY = 1000; // 1 second

function initChat(sessionInfo) {
    wsInstance = connectWebSocket(
        sessionInfo,
        handleIncomingMessage,
        (error) => {
            console.error("Connection error, attempting reconnect...", error);
            attemptReconnect(sessionInfo);
        }
    );
}

function attemptReconnect(sessionInfo) {
    if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
        alert("Max reconnect attempts reached. Please refresh the page.");
        return;
    }

    const delay = BASE_DELAY * Math.pow(2, reconnectAttempts);
    console.log(`Reconnecting in ${delay}ms...`);
    
    setTimeout(() => {
        reconnectAttempts++;
        initChat(sessionInfo);
    }, delay);
}

Complete Working Example

This is a single-file HTML/JS example. Save this as index.html and open it in a browser. You must replace YOUR_ACCESS_TOKEN and YOUR_ORGANIZATION_ID with valid values.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Genesys Cloud Custom Chat</title>
    <style>
        body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; }
        #chat-container { border: 1px solid #ccc; height: 400px; overflow-y: scroll; padding: 1rem; margin-bottom: 1rem; }
        .message { margin-bottom: 0.5rem; padding: 0.5rem; border-radius: 4px; }
        .user { background-color: #e3f2fd; align-self: flex-end; text-align: right; }
        .agent { background-color: #f5f5f5; }
        .typing { font-style: italic; color: #888; font-size: 0.8rem; }
        #input-area { display: flex; gap: 0.5rem; }
        input { flex-grow: 1; padding: 0.5rem; }
        button { padding: 0.5rem 1rem; background-color: #007bff; color: white; border: none; cursor: pointer; }
        button:disabled { background-color: #ccc; }
        #status { margin-bottom: 1rem; font-weight: bold; }
    </style>
</head>
<body>

    <h1>Custom Genesys Chat</h1>
    <div id="status">Connecting...</div>
    <div id="chat-container"></div>
    
    <div id="input-area">
        <input type="text" id="message-input" placeholder="Type a message..." disabled>
        <button id="send-btn" disabled>Send</button>
    </div>

    <script>
        // CONFIGURATION
        const ACCESS_TOKEN = "YOUR_ACCESS_TOKEN_HERE"; // Replace with valid token
        const ORGANIZATION_ID = "YOUR_ORGANIZATION_ID_HERE"; // Replace with valid Org ID
        const REGION = "us-east-1"; // Match your region

        let wsInstance = null;
        let sessionInfo = null;
        let reconnectAttempts = 0;

        // UI Elements
        const chatContainer = document.getElementById('chat-container');
        const messageInput = document.getElementById('message-input');
        const sendBtn = document.getElementById('send-btn');
        const statusDiv = document.getElementById('status');

        // 1. Initialize Session
        async function initSession() {
            try {
                statusDiv.textContent = "Creating session...";
                sessionInfo = await createGuestSession(ACCESS_TOKEN, ORGANIZATION_ID);
                statusDiv.textContent = "Session created. Connecting to WebSocket...";
                initChat();
            } catch (error) {
                statusDiv.textContent = `Error: ${error.message}`;
                console.error(error);
            }
        }

        // 2. Create Guest Session via REST
        async function createGuestSession(accessToken, organizationId) {
            const url = `https://api.${REGION}.mygen.com/api/v2/webmessaging/guest/sessions`;
            const headers = {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${accessToken}`,
                'X-Genesys-Organization': organizationId
            };

            const response = await fetch(url, {
                method: 'POST',
                headers: headers,
                body: JSON.stringify({
                    "guestName": "Web Dev",
                    "guestEmail": "dev@example.com"
                })
            });

            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            const data = await response.json();
            return {
                guestToken: data.guest_token,
                organizationId: data.organization_id,
                sessionId: data.session_id
            };
        }

        // 3. Connect WebSocket
        function initChat() {
            const wsUrl = `wss://webmessaging.${REGION}.mygen.com/api/v2/webmessaging/guest/ws?guest_token=${sessionInfo.guestToken}&organization_id=${sessionInfo.organizationId}`;
            
            wsInstance = new WebSocket(wsUrl);

            wsInstance.onopen = () => {
                statusDiv.textContent = "Connected";
                enableInput();
                addMessageToUI("system", "Chat connected. You can start typing.");
            };

            wsInstance.onmessage = (event) => {
                const message = JSON.parse(event.data);
                handleIncomingMessage(message);
            };

            wsInstance.onclose = (event) => {
                statusDiv.textContent = "Disconnected";
                disableInput();
                if (event.code !== 1000) { // Not a clean close
                    attemptReconnect();
                }
            };

            wsInstance.onerror = (error) => {
                console.error("WebSocket error", error);
            };
        }

        // 4. Handle Messages
        function handleIncomingMessage(message) {
            switch (message.type) {
                case 'agent_message':
                    addMessageToUI("agent", message.text);
                    break;
                case 'agent_typing':
                    updateTypingIndicator(true);
                    break;
                case 'agent_not_typing':
                    updateTypingIndicator(false);
                    break;
                case 'routing':
                    if (message.status === 'queued') {
                        addMessageToUI("system", "You are in the queue.");
                    } else if (message.status === 'connected') {
                        addMessageToUI("system", "Connected to an agent.");
                    }
                    break;
                default:
                    console.log("Unhandled message type:", message.type);
            }
        }

        // 5. Send Message
        function sendMessage() {
            const text = messageInput.value.trim();
            if (!text || !wsInstance || wsInstance.readyState !== WebSocket.OPEN) return;

            const payload = {
                "type": "message",
                "session_id": sessionInfo.sessionId,
                "text": text
            };

            wsInstance.send(JSON.stringify(payload));
            addMessageToUI("user", text);
            messageInput.value = "";
        }

        // UI Helpers
        function addMessageToUI(sender, text) {
            const div = document.createElement('div');
            div.className = `message ${sender}`;
            div.textContent = text;
            chatContainer.appendChild(div);
            chatContainer.scrollTop = chatContainer.scrollHeight;
        }

        function updateTypingIndicator(isTyping) {
            let typingDiv = document.getElementById('typing-indicator');
            if (isTyping) {
                if (!typingDiv) {
                    typingDiv = document.createElement('div');
                    typingDiv.id = 'typing-indicator';
                    typingDiv.className = 'typing';
                    typingDiv.textContent = "Agent is typing...";
                    chatContainer.appendChild(typingDiv);
                }
                chatContainer.scrollTop = chatContainer.scrollHeight;
            } else {
                if (typingDiv) typingDiv.remove();
            }
        }

        function enableInput() {
            messageInput.disabled = false;
            sendBtn.disabled = false;
            messageInput.focus();
        }

        function disableInput() {
            messageInput.disabled = true;
            sendBtn.disabled = true;
        }

        function attemptReconnect() {
            if (reconnectAttempts > 5) {
                statusDiv.textContent = "Failed to reconnect.";
                return;
            }
            reconnectAttempts++;
            setTimeout(initChat, 2000 * reconnectAttempts);
        }

        // Event Listeners
        sendBtn.addEventListener('click', sendMessage);
        messageInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') sendMessage();
        });

        // Start
        initSession();
    </script>
</body>
</html>

Common Errors & Debugging

Error: 401 Unauthorized on REST Call

  • Cause: The access_token provided to createGuestSession is invalid, expired, or lacks the webmessaging:guest scope.
  • Fix: Verify the token in a JWT decoder. Ensure the OAuth client used to generate the token has the webmessaging:guest scope enabled in the Genesys Cloud Admin > Security > OAuth Clients.

Error: WebSocket Connection Refused / 403 on WS Upgrade

  • Cause: The guest_token is invalid, expired, or the organization_id in the WebSocket URL does not match the organization associated with the token.
  • Fix: Check the expiration time of the guest_token. Ensure you are passing the correct organization_id from the REST response into the WebSocket URL query parameters.

Error: session_id Mismatch

  • Cause: You are sending messages with a session_id that does not match the one returned during the POST /guest/sessions call.
  • Fix: Ensure you capture the session_id from the initial REST response and reuse it exactly for all subsequent WebSocket messages. Do not generate a new UUID client-side.

Error: Messages Not Appearing

  • Cause: The routing configuration in Genesys Cloud does not have a skill or queue assigned to the web messaging channel, or the agent is not available.
  • Fix: Check the routing message types in the console. If you receive routing with status: 'queued' but no connected event, check your Genesys Cloud Workforce Management or Routing configuration to ensure agents are online and assigned to the correct skill.

Official References