Build a Custom Chat UI Using the Genesys Cloud WebSocket Guest API

Build a Custom Chat UI Using the Genesys Cloud WebSocket Guest API

What You Will Build

You will build a fully functional, custom web-based chat interface that connects directly to Genesys Cloud via the WebSocket Guest API, bypassing the standard embedded widget. This tutorial uses JavaScript (ES6+) with the fetch API for authentication and the native WebSocket object for real-time messaging. The language is JavaScript.

Prerequisites

  • OAuth Client Type: Public Client (Confidential clients are not recommended for browser-side JavaScript due to secret exposure).
  • Required OAuth Scope: chat:guest:create is required to initiate the session. webchat:guest is often implicitly handled by the WebSocket handshake once the session is established, but verify your client permissions.
  • API Version: Genesys Cloud API v2.
  • Runtime: Any modern web browser (Chrome, Firefox, Edge, Safari) or Node.js 18+ (if using a WebSocket library compatible with Node).
  • Dependencies: None. This tutorial uses native browser APIs. No external SDKs are required for the WebSocket implementation, though the official @genesyscloud/purecloud-platform-client-v2-javascript SDK can be used for the initial OAuth token exchange if preferred. We will use raw fetch for clarity.

Authentication Setup

The WebSocket Guest API does not use a traditional “login” screen. Instead, it uses a two-step process:

  1. Obtain an OAuth Access Token: Use the Client Credentials flow or Authorization Code flow to get a bearer token.
  2. Create a Guest Session: Call the REST API to create a guestSession. This returns a sessionId and a token (a JWT-like string) that authorizes the subsequent WebSocket connection.

Step 1: Obtain OAuth Token

You must have a Public OAuth Client registered in your Genesys Cloud organization. The client must have the chat:guest:create scope.

const GENESYS_ORGANIZATION_ID = 'your-organization-id';
const GENESYS_CLIENT_ID = 'your-client-id';
const GENESYS_CLIENT_SECRET = 'your-client-secret'; // Only for server-side or public clients with secret

async function getAccessToken() {
    const url = `https://api.mypurecloud.com/api/v2/oauth/token`;
    
    const params = new URLSearchParams();
    params.append('grant_type', 'client_credentials');
    params.append('client_id', GENESYS_CLIENT_ID);
    params.append('client_secret', GENESYS_CLIENT_SECRET);
    params.append('scope', 'chat:guest:create');

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: params
        });

        if (!response.ok) {
            throw new Error(`OAuth failed: ${response.status} ${response.statusText}`);
        }

        const data = await response.json();
        return data.access_token;
    } catch (error) {
        console.error('Error fetching access token:', error);
        throw error;
    }
}

Step 2: Create Guest Session

Once you have the access token, you must create a guest session. This endpoint returns the sessionId and token required for the WebSocket handshake.

Endpoint: POST /api/v2/chat/guestsessions
Scope: chat:guest:create

async function createGuestSession(accessToken) {
    const url = `https://api.mypurecloud.com/api/v2/chat/guestsessions`;

    const payload = {
        "externalContactId": "user-unique-id-123", // Optional: Link to your CRM
        "name": "John Doe",                        // Optional: Display name
        "email": "john.doe@example.com"            // Optional: Email address
    };

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${accessToken}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`Guest Session creation failed: ${response.status} - ${errorText}`);
        }

        const sessionData = await response.json();
        return {
            sessionId: sessionData.sessionId,
            token: sessionData.token,
            externalContactId: sessionData.externalContactId
        };
    } catch (error) {
        console.error('Error creating guest session:', error);
        throw error;
    }
}

Implementation

Step 1: Establish the WebSocket Connection

The WebSocket URL is constructed using the sessionId and token from the previous step. The URL format is strict.

WebSocket URL Pattern:
wss://chat.mypurecloud.com/ws?sessionId={sessionId}&token={token}

Note: If your organization uses a specific region (e.g., Amazon Web Services Asia Pacific), the subdomain may change (e.g., chat.ap.mypurecloud.com). For US/EU, chat.mypurecloud.com is standard.

class GenesysChatClient {
    constructor() {
        this.ws = null;
        this.isConnected = false;
        this.messageQueue = []; // Queue messages sent before connection is fully open
    }

    connect(sessionId, token) {
        const wsUrl = `wss://chat.mypurecloud.com/ws?sessionId=${sessionId}&token=${token}`;
        
        this.ws = new WebSocket(wsUrl);

        this.ws.onopen = () => {
            console.log('WebSocket connected to Genesys Cloud');
            this.isConnected = true;
            this.flushMessageQueue();
        };

        this.ws.onmessage = (event) => {
            this.handleIncomingMessage(event.data);
        };

        this.ws.onerror = (error) => {
            console.error('WebSocket error:', error);
            this.isConnected = false;
        };

        this.ws.onclose = (event) => {
            console.log(`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`);
            this.isConnected = false;
            // Implement reconnection logic here if desired
        };
    }

    flushMessageQueue() {
        while (this.messageQueue.length > 0) {
            const msg = this.messageQueue.shift();
            this.ws.send(JSON.stringify(msg));
        }
    }
}

Step 2: Handle Incoming Messages

Genesys Cloud sends various message types over the WebSocket. The most critical ones are:

  1. conversation-update: Metadata about the conversation (status, participants).
  2. message: Actual chat text.
  3. bot-message: Messages from the virtual agent.
  4. typing: Indication that the agent is typing.

You must parse the JSON payload and route it to your UI handler.

    handleIncomingMessage(rawData) {
        let data;
        try {
            data = JSON.parse(rawData);
        } catch (e) {
            console.error('Failed to parse incoming message:', rawData);
            return;
        }

        console.log('Received message type:', data.type);

        switch (data.type) {
            case 'message':
                this.onChatMessage(data);
                break;
            case 'conversation-update':
                this.onConversationUpdate(data);
                break;
            case 'typing':
                this.onTypingIndicator(data);
                break;
            case 'bot-message':
                this.onBotMessage(data);
                break;
            default:
                console.warn('Unhandled message type:', data.type);
        }
    }

    onChatMessage(data) {
        // Data structure example:
        // {
        //   "type": "message",
        //   "from": "agent",
        //   "text": "Hello, how can I help you?",
        //   "timestamp": "2023-10-27T10:00:00Z"
        // }
        console.log(`Message from ${data.from}: ${data.text}`);
        // TODO: Update UI with this message
    }

    onConversationUpdate(data) {
        // Data structure example:
        // {
        //   "type": "conversation-update",
        //   "status": "connected", // or "waiting", "ended"
        //   "agentName": "Jane Smith"
        // }
        console.log(`Conversation status changed to: ${data.status}`);
        if (data.agentName) {
            console.log(`Connected to agent: ${data.agentName}`);
        }
        // TODO: Update UI status indicator (e.g., show "Connected" or "Agent Typing")
    }

    onTypingIndicator(data) {
        console.log(`${data.from} is typing...`);
        // TODO: Show typing indicator in UI
    }
    
    onBotMessage(data) {
        // Often similar to 'message' but may contain rich content payloads
        console.log(`Bot message: ${data.text}`);
    }

Step 3: Send Messages

To send a message, you must construct a specific JSON object. The type field must be message. The from field is usually omitted or set to guest, as the server identifies the sender via the session token.

    sendMessage(text) {
        if (!this.isConnected) {
            console.warn('Not connected. Queueing message.');
            this.messageQueue.push({
                type: 'message',
                text: text
            });
            return;
        }

        const messagePayload = {
            type: 'message',
            text: text
        };

        try {
            this.ws.send(JSON.stringify(messagePayload));
        } catch (error) {
            console.error('Failed to send message:', error);
        }
    }

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 the placeholder constants with your actual Genesys Cloud credentials.

Security Warning: This example exposes your Client Secret in the browser console. For production, you must proxy the OAuth token request through your own backend server to keep the secret hidden.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom Genesys Chat UI</title>
    <style>
        body { font-family: sans-serif; padding: 20px; max-width: 600px; margin: 0 auto; }
        #chat-window { border: 1px solid #ccc; height: 400px; overflow-y: scroll; padding: 10px; margin-bottom: 10px; }
        .message { margin: 5px 0; padding: 8px; border-radius: 5px; }
        .message.agent { background-color: #e0f7fa; align-self: flex-start; }
        .message.guest { background-color: #f0f0f0; text-align: right; }
        .system { color: #888; font-style: italic; font-size: 0.9em; text-align: center; }
        #input-area { display: flex; gap: 10px; }
        #message-input { flex-grow: 1; padding: 10px; }
        button { padding: 10px 20px; cursor: pointer; }
        button:disabled { opacity: 0.5; cursor: not-allowed; }
    </style>
</head>
<body>

    <h2>Genesys Cloud Custom Chat</h2>
    <div id="status">Disconnected</div>
    <div id="chat-window"></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 GENESYS_ORGANIZATION_ID = 'your-organization-id';
        const GENESYS_CLIENT_ID = 'your-client-id';
        const GENESYS_CLIENT_SECRET = 'your-client-secret';

        class GenesysChatClient {
            constructor() {
                this.ws = null;
                this.isConnected = false;
                this.messageQueue = [];
                this.sessionId = null;
            }

            async init() {
                try {
                    document.getElementById('status').innerText = 'Authenticating...';
                    
                    // Step 1: Get Token
                    const token = await this.getAccessToken();
                    document.getElementById('status').innerText = 'Creating Session...';

                    // Step 2: Create Session
                    const session = await this.createGuestSession(token);
                    this.sessionId = session.sessionId;
                    
                    document.getElementById('status').innerText = 'Connecting to WebSocket...';
                    
                    // Step 3: Connect WebSocket
                    this.connect(session.sessionId, session.token);

                } catch (error) {
                    document.getElementById('status').innerText = `Error: ${error.message}`;
                    console.error(error);
                }
            }

            async getAccessToken() {
                const url = `https://api.mypurecloud.com/api/v2/oauth/token`;
                const params = new URLSearchParams();
                params.append('grant_type', 'client_credentials');
                params.append('client_id', GENESYS_CLIENT_ID);
                params.append('client_secret', GENESYS_CLIENT_SECRET);
                params.append('scope', 'chat:guest:create');

                const response = await fetch(url, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: params
                });

                if (!response.ok) throw new Error(`OAuth failed: ${response.status}`);
                const data = await response.json();
                return data.access_token;
            }

            async createGuestSession(accessToken) {
                const url = `https://api.mypurecloud.com/api/v2/chat/guestsessions`;
                const payload = {
                    name: "Guest User",
                    email: "guest@example.com"
                };

                const response = await fetch(url, {
                    method: 'POST',
                    headers: {
                        'Authorization': `Bearer ${accessToken}`,
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(payload)
                });

                if (!response.ok) {
                    const err = await response.text();
                    throw new Error(`Session creation failed: ${err}`);
                }

                const data = await response.json();
                return { sessionId: data.sessionId, token: data.token };
            }

            connect(sessionId, token) {
                const wsUrl = `wss://chat.mypurecloud.com/ws?sessionId=${sessionId}&token=${token}`;
                this.ws = new WebSocket(wsUrl);

                this.ws.onopen = () => {
                    this.isConnected = true;
                    this.updateStatus('Connected');
                    this.enableInput();
                    this.addSystemMessage('Connected to Genesys Cloud');
                    this.flushMessageQueue();
                };

                this.ws.onmessage = (event) => {
                    const data = JSON.parse(event.data);
                    this.handleMessage(data);
                };

                this.ws.onclose = () => {
                    this.isConnected = false;
                    this.updateStatus('Disconnected');
                    this.disableInput();
                };
            }

            handleMessage(data) {
                if (data.type === 'message') {
                    this.addMessage(data.text, data.from === 'agent' ? 'agent' : 'guest');
                } else if (data.type === 'conversation-update') {
                    if (data.status === 'connected') {
                        this.addSystemMessage(`Agent ${data.agentName || 'an agent'} has joined.`);
                    } else if (data.status === 'ended') {
                        this.addSystemMessage('Conversation ended.');
                        this.disableInput();
                    }
                } else if (data.type === 'typing') {
                    // Optional: Implement typing indicator UI
                }
            }

            sendMessage(text) {
                if (!text.trim()) return;
                
                const payload = { type: 'message', text: text };
                
                if (this.isConnected) {
                    this.ws.send(JSON.stringify(payload));
                    this.addMessage(text, 'guest');
                } else {
                    this.messageQueue.push(payload);
                }
                
                document.getElementById('message-input').value = '';
            }

            flushMessageQueue() {
                while (this.messageQueue.length > 0) {
                    this.ws.send(JSON.stringify(this.messageQueue.shift()));
                }
            }

            // UI Helpers
            addMessage(text, sender) {
                const chatWindow = document.getElementById('chat-window');
                const div = document.createElement('div');
                div.className = `message ${sender}`;
                div.innerText = text;
                chatWindow.appendChild(div);
                chatWindow.scrollTop = chatWindow.scrollHeight;
            }

            addSystemMessage(text) {
                const chatWindow = document.getElementById('chat-window');
                const div = document.createElement('div');
                div.className = 'system';
                div.innerText = text;
                chatWindow.appendChild(div);
                chatWindow.scrollTop = chatWindow.scrollHeight;
            }

            updateStatus(status) {
                document.getElementById('status').innerText = status;
            }

            enableInput() {
                document.getElementById('message-input').disabled = false;
                document.getElementById('send-btn').disabled = false;
            }

            disableInput() {
                document.getElementById('message-input').disabled = true;
                document.getElementById('send-btn').disabled = true;
            }
        }

        // Initialize
        const chatClient = new GenesysChatClient();
        
        document.getElementById('send-btn').addEventListener('click', () => {
            const input = document.getElementById('message-input');
            chatClient.sendMessage(input.value);
        });

        document.getElementById('message-input').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                chatClient.sendMessage(e.target.value);
            }
        });

        chatClient.init();
    </script>
</body>
</html>

Common Errors & Debugging

Error: 401 Unauthorized on Guest Session Creation

Cause: The OAuth token used to call POST /api/v2/chat/guestsessions is expired, invalid, or missing the chat:guest:create scope.
Fix: Verify your OAuth client has the correct scope. Check the expiration time of the token (usually 1 hour). Ensure you are passing the token in the Authorization: Bearer <token> header.

Error: WebSocket Connection Refused or 403

Cause: The sessionId or token passed in the WebSocket URL is invalid, expired, or mismatched.
Fix: Ensure the token used in the WebSocket URL is the token field returned from the Guest Session API response, not the OAuth access_token. They are different. The OAuth token creates the session; the session token authenticates the WebSocket.

Error: Messages Sent but Not Received by Agent

Cause: The chat flow in Genesys Cloud may not be routing the conversation to an available queue or agent.
Fix: Check the Genesys Cloud Admin UI for the Chat Flow. Ensure the flow is published and that there are agents available in the target queue. The WebSocket connection will succeed even if no agent is available, but the agent won’t see the messages until the flow routes the conversation.

Error: CORS Issues on OAuth Request

Cause: Browsers block cross-origin requests to api.mypurecloud.com if the request contains credentials or custom headers that trigger a preflight check, and the browser security policy restricts it.
Fix: Genesys Cloud supports CORS for standard REST APIs. However, for security, it is best practice to proxy the OAuth token request through your own backend. If testing locally, ensure your local server is configured correctly.

Official References