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

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

What You Will Build

  • You will build a functional, custom chat interface that connects directly to Genesys Cloud using the WebSocket-based Guest API.
  • You will bypass the standard @genesyscloud/purecloud-chat-widget package to gain full control over the UI rendering and event handling.
  • You will use JavaScript (ES6+) and the native WebSocket API to handle authentication, message sending, and real-time updates.

Prerequisites

  • OAuth Client: You must have a Genesys Cloud OAuth client configured with the client_credentials grant type.
  • Required Scopes: The client must have the webchat:guest scope. This is critical. Without this scope, the WebSocket connection will be rejected during the handshake.
  • Organization ID: Your Genesys Cloud Organization ID (found in the Admin console under Settings > Organization).
  • Runtime: A modern web browser or Node.js environment with WebSocket support. For this tutorial, we will use a browser-based approach as it is the most common use case for chat UIs.
  • Dependencies: None. This tutorial uses vanilla JavaScript and the native WebSocket object. No external libraries are required.

Authentication Setup

The Genesys Cloud Guest WebSocket API does not use the standard HTTP OAuth token for the initial connection. Instead, it requires a specific handshake process where you authenticate via HTTP first to obtain a guestToken, and then use that token to establish the WebSocket connection.

Step 1: Obtain the OAuth Access Token

Before connecting to the WebSocket, you must obtain a standard OAuth access token from the Genesys Cloud Authorization Server. This token is used to request the guestToken.

Endpoint: POST https://api.mypurecloud.com/api/v2/authorization/token
Method: POST
Content-Type: application/x-www-form-urlencoded

async function getOAuthAccessToken(clientId, clientSecret) {
    const baseUrl = 'https://api.mypurecloud.com';
    const tokenEndpoint = `${baseUrl}/api/v2/authorization/token`;

    const params = new URLSearchParams();
    params.append('grant_type', 'client_credentials');
    params.append('client_id', clientId);
    params.append('client_secret', clientSecret);

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

        if (!response.ok) {
            const errorBody = await response.text();
            throw new Error(`OAuth Token Request Failed: ${response.status} - ${errorBody}`);
        }

        const data = await response.json();
        return data.access_token;
    } catch (error) {
        console.error('Failed to obtain OAuth access token:', error);
        throw error;
    }
}

Important: The client_id and client_secret must correspond to an OAuth client that has the webchat:guest scope. If you do not have this scope, the subsequent step to get the guest token will fail with a 403 Forbidden error.

Step 2: Obtain the Guest Token

Once you have the OAuth access token, you must exchange it for a guestToken. This token is specific to the WebSocket Guest API and has a limited lifetime.

Endpoint: POST https://api.mypurecloud.com/api/v2/webchat/guest/tokens
Method: POST
Authorization: Bearer <oauth_access_token>

async function getGuestToken(oauthAccessToken, organizationId) {
    const baseUrl = 'https://api.mypurecloud.com';
    const guestTokenEndpoint = `${baseUrl}/api/v2/webchat/guest/tokens`;

    const requestBody = {
        organizationId: organizationId
    };

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

        if (!response.ok) {
            const errorBody = await response.text();
            throw new Error(`Guest Token Request Failed: ${response.status} - ${errorBody}`);
        }

        const data = await response.json();
        return data.token; // This is the guestToken
    } catch (error) {
        console.error('Failed to obtain guest token:', error);
        throw error;
    }
}

Response Structure:
The response from /api/v2/webchat/guest/tokens looks like this:

{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": 3600
}

You must use the token value in the WebSocket handshake.

Implementation

Step 1: Establish the WebSocket Connection

The Genesys Cloud Guest WebSocket API uses a specific endpoint structure. The URL includes the guestToken as a query parameter.

WebSocket URL: wss://webchat.mypurecloud.com/api/v2/webchat/guest?token=<guestToken>

Note: The host is webchat.mypurecloud.com, not api.mypurecloud.com. This is a common source of connection failures.

class GenesysCloudChatClient {
    constructor(organizationId, clientId, clientSecret) {
        this.organizationId = organizationId;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.ws = null;
        this.guestToken = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.reconnectDelay = 1000;
    }

    async connect() {
        try {
            // Step 1: Get OAuth Access Token
            console.log('Obtaining OAuth access token...');
            const oauthAccessToken = await this.getOAuthAccessToken(this.clientId, this.clientSecret);
            
            // Step 2: Get Guest Token
            console.log('Obtaining guest token...');
            this.guestToken = await this.getGuestToken(oauthAccessToken, this.organizationId);
            
            // Step 3: Connect to WebSocket
            console.log('Connecting to WebSocket...');
            this.connectWebSocket();
        } catch (error) {
            console.error('Connection failed:', error);
            throw error;
        }
    }

    connectWebSocket() {
        const wsUrl = `wss://webchat.mypurecloud.com/api/v2/webchat/guest?token=${this.guestToken}`;
        
        this.ws = new WebSocket(wsUrl);

        this.ws.onopen = () => {
            console.log('WebSocket connection established');
            this.reconnectAttempts = 0;
            // Send initial configuration if needed
            this.send({
                type: 'configuration',
                payload: {
                    capabilities: ['message', 'typing']
                }
            });
        };

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

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

        this.ws.onclose = (event) => {
            console.log(`WebSocket closed: Code ${event.code}, Reason: ${event.reason}`);
            this.handleReconnect();
        };
    }

    // Helper methods for OAuth and Guest Token are included here for completeness
    // In production, move these to a separate service module
    async getOAuthAccessToken(clientId, clientSecret) {
        const baseUrl = 'https://api.mypurecloud.com';
        const tokenEndpoint = `${baseUrl}/api/v2/authorization/token`;
        const params = new URLSearchParams();
        params.append('grant_type', 'client_credentials');
        params.append('client_id', clientId);
        params.append('client_secret', clientSecret);

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

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

    async getGuestToken(oauthAccessToken, organizationId) {
        const baseUrl = 'https://api.mypurecloud.com';
        const guestTokenEndpoint = `${baseUrl}/api/v2/webchat/guest/tokens`;
        const requestBody = { organizationId: organizationId };

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

        if (!response.ok) throw new Error(`Guest Token Request Failed: ${response.status}`);
        const data = await response.json();
        return data.token;
    }
}

Step 2: Handle Incoming Messages

The WebSocket server sends JSON messages for various events. You must parse these messages and update your UI accordingly. The most common message types are:

  1. message: A new message from an agent or bot.
  2. typing: An agent is typing.
  3. status: The conversation status has changed (e.g., connected, queued, closed).
  4. configuration: Initial configuration data.
    handleMessage(data) {
        let message;
        try {
            message = JSON.parse(data);
        } catch (e) {
            console.error('Failed to parse WebSocket message:', e);
            return;
        }

        switch (message.type) {
            case 'message':
                this.handleIncomingMessage(message);
                break;
            case 'typing':
                this.handleTypingIndicator(message);
                break;
            case 'status':
                this.handleStatusUpdate(message);
                break;
            case 'configuration':
                this.handleConfiguration(message);
                break;
            default:
                console.log('Unknown message type:', message.type);
        }
    }

    handleIncomingMessage(message) {
        // message.payload contains the actual message details
        // Example payload: { id: 'msg-123', author: 'agent', text: 'Hello!', timestamp: '2023-10-27T10:00:00Z' }
        console.log('Received message:', message.payload);
        
        // Update UI
        if (typeof window !== 'undefined' && window.updateChatUI) {
            window.updateChatUI(message.payload);
        }
    }

    handleTypingIndicator(message) {
        // message.payload contains { isTyping: true/false }
        console.log('Typing indicator:', message.payload.isTyping);
        if (typeof window !== 'undefined' && window.updateTypingIndicator) {
            window.updateTypingIndicator(message.payload.isTyping);
        }
    }

    handleStatusUpdate(message) {
        // message.payload contains { status: 'connected' }
        console.log('Status update:', message.payload.status);
        if (typeof window !== 'undefined' && window.updateChatStatus) {
            window.updateChatStatus(message.payload.status);
        }
    }

    handleConfiguration(message) {
        console.log('Configuration received:', message.payload);
        // Store configuration for later use
        this.configuration = message.payload;
    }

Step 3: Send Messages

To send a message to the agent, you must send a JSON object with the type message and a payload containing the text and id. The id must be a unique identifier for the message, typically a UUID.

    sendMessage(text) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            const messageId = this.generateUUID();
            const messagePayload = {
                type: 'message',
                payload: {
                    id: messageId,
                    author: 'guest',
                    text: text,
                    timestamp: new Date().toISOString()
                }
            };

            console.log('Sending message:', messagePayload);
            this.ws.send(JSON.stringify(messagePayload));
            
            // Update local UI to show the sent message immediately
            if (typeof window !== 'undefined' && window.updateChatUI) {
                window.updateChatUI(messagePayload.payload);
            }
        } else {
            console.error('WebSocket is not open');
            throw new Error('WebSocket is not open');
        }
    }

    generateUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            const r = Math.random() * 16 | 0;
            const v = c === 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

Step 4: Handle Reconnection

WebSocket connections can drop due to network issues or token expiration. You must implement a reconnection strategy. The guestToken expires after a certain period (typically 1 hour). When the token expires, the WebSocket server will close the connection. You must obtain a new guestToken and reconnect.

    handleReconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); // Exponential backoff
            console.log(`Reconnecting in ${delay}ms... (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
            
            setTimeout(async () => {
                try {
                    // Refresh guest token before reconnecting
                    console.log('Refreshing guest token...');
                    const oauthAccessToken = await this.getOAuthAccessToken(this.clientId, this.clientSecret);
                    this.guestToken = await this.getGuestToken(oauthAccessToken, this.organizationId);
                    this.connectWebSocket();
                } catch (error) {
                    console.error('Reconnection failed:', error);
                    // Try again after delay
                    this.handleReconnect();
                }
            }, delay);
        } else {
            console.error('Max reconnection attempts reached');
            // Notify user
            if (typeof window !== 'undefined' && window.notifyUser) {
                window.notifyUser('Connection lost. Please refresh the page.');
            }
        }
    }

Complete Working Example

This is a complete, copy-pasteable HTML file that includes the GenesysCloudChatClient class and a simple UI. Save this as index.html and open it in a browser. You must replace YOUR_CLIENT_ID, YOUR_CLIENT_SECRET, and YOUR_ORGANIZATION_ID with your actual 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: 0 auto; padding: 20px; }
        #chat-container { border: 1px solid #ccc; padding: 10px; height: 400px; overflow-y: auto; margin-bottom: 10px; }
        .message { margin: 5px 0; padding: 5px 10px; border-radius: 5px; }
        .message.guest { background-color: #e0f7fa; text-align: right; }
        .message.agent { background-color: #f5f5f5; text-align: left; }
        #input-container { display: flex; }
        #message-input { flex-grow: 1; padding: 10px; margin-right: 10px; }
        #send-button { padding: 10px 20px; background-color: #007bff; color: white; border: none; cursor: pointer; }
        #status { margin-bottom: 10px; color: #666; font-style: italic; }
        #typing-indicator { margin-bottom: 10px; color: #999; font-style: italic; display: none; }
    </style>
</head>
<body>
    <h1>Genesys Cloud Custom Chat</h1>
    <div id="status">Disconnected</div>
    <div id="typing-indicator">Agent is typing...</div>
    <div id="chat-container"></div>
    <div id="input-container">
        <input type="text" id="message-input" placeholder="Type your message..." />
        <button id="send-button">Send</button>
    </div>

    <script>
        class GenesysCloudChatClient {
            constructor(organizationId, clientId, clientSecret) {
                this.organizationId = organizationId;
                this.clientId = clientId;
                this.clientSecret = clientSecret;
                this.ws = null;
                this.guestToken = null;
                this.reconnectAttempts = 0;
                this.maxReconnectAttempts = 5;
                this.reconnectDelay = 1000;
            }

            async connect() {
                try {
                    const oauthAccessToken = await this.getOAuthAccessToken(this.clientId, this.clientSecret);
                    this.guestToken = await this.getGuestToken(oauthAccessToken, this.organizationId);
                    this.connectWebSocket();
                } catch (error) {
                    console.error('Connection failed:', error);
                    document.getElementById('status').innerText = 'Connection failed: ' + error.message;
                }
            }

            connectWebSocket() {
                const wsUrl = `wss://webchat.mypurecloud.com/api/v2/webchat/guest?token=${this.guestToken}`;
                this.ws = new WebSocket(wsUrl);

                this.ws.onopen = () => {
                    document.getElementById('status').innerText = 'Connected';
                    this.reconnectAttempts = 0;
                    this.send({ type: 'configuration', payload: { capabilities: ['message', 'typing'] } });
                };

                this.ws.onmessage = (event) => this.handleMessage(event.data);
                this.ws.onerror = (error) => console.error('WebSocket error:', error);
                this.ws.onclose = (event) => this.handleReconnect();
            }

            handleMessage(data) {
                let message;
                try {
                    message = JSON.parse(data);
                } catch (e) {
                    return;
                }

                switch (message.type) {
                    case 'message':
                        window.updateChatUI(message.payload);
                        break;
                    case 'typing':
                        window.updateTypingIndicator(message.payload.isTyping);
                        break;
                    case 'status':
                        window.updateChatStatus(message.payload.status);
                        break;
                }
            }

            sendMessage(text) {
                if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                    const messageId = this.generateUUID();
                    const messagePayload = {
                        type: 'message',
                        payload: {
                            id: messageId,
                            author: 'guest',
                            text: text,
                            timestamp: new Date().toISOString()
                        }
                    };
                    this.ws.send(JSON.stringify(messagePayload));
                    window.updateChatUI(messagePayload.payload);
                }
            }

            generateUUID() {
                return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                    const r = Math.random() * 16 | 0;
                    const v = c === 'x' ? r : (r & 0x3 | 0x8);
                    return v.toString(16);
                });
            }

            async getOAuthAccessToken(clientId, clientSecret) {
                const baseUrl = 'https://api.mypurecloud.com';
                const tokenEndpoint = `${baseUrl}/api/v2/authorization/token`;
                const params = new URLSearchParams();
                params.append('grant_type', 'client_credentials');
                params.append('client_id', clientId);
                params.append('client_secret', clientSecret);

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

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

            async getGuestToken(oauthAccessToken, organizationId) {
                const baseUrl = 'https://api.mypurecloud.com';
                const guestTokenEndpoint = `${baseUrl}/api/v2/webchat/guest/tokens`;
                const requestBody = { organizationId: organizationId };

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

                if (!response.ok) throw new Error(`Guest Token Request Failed: ${response.status}`);
                const data = await response.json();
                return data.token;
            }

            handleReconnect() {
                if (this.reconnectAttempts < this.maxReconnectAttempts) {
                    this.reconnectAttempts++;
                    const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
                    setTimeout(async () => {
                        try {
                            const oauthAccessToken = await this.getOAuthAccessToken(this.clientId, this.clientSecret);
                            this.guestToken = await this.getGuestToken(oauthAccessToken, this.organizationId);
                            this.connectWebSocket();
                        } catch (error) {
                            this.handleReconnect();
                        }
                    }, delay);
                } else {
                    document.getElementById('status').innerText = 'Connection lost. Please refresh.';
                }
            }
        }

        // UI Update Functions
        window.updateChatUI = (payload) => {
            const chatContainer = document.getElementById('chat-container');
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${payload.author}`;
            messageDiv.innerText = payload.text;
            chatContainer.appendChild(messageDiv);
            chatContainer.scrollTop = chatContainer.scrollHeight;
        };

        window.updateTypingIndicator = (isTyping) => {
            const typingIndicator = document.getElementById('typing-indicator');
            typingIndicator.style.display = isTyping ? 'block' : 'none';
        };

        window.updateChatStatus = (status) => {
            document.getElementById('status').innerText = `Status: ${status}`;
        };

        // Initialize Client
        const clientId = 'YOUR_CLIENT_ID';
        const clientSecret = 'YOUR_CLIENT_SECRET';
        const organizationId = 'YOUR_ORGANIZATION_ID';

        const chatClient = new GenesysCloudChatClient(organizationId, clientId, clientSecret);
        chatClient.connect();

        // Event Listeners
        document.getElementById('send-button').addEventListener('click', () => {
            const input = document.getElementById('message-input');
            const text = input.value.trim();
            if (text) {
                chatClient.sendMessage(text);
                input.value = '';
            }
        });

        document.getElementById('message-input').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                const input = document.getElementById('message-input');
                const text = input.value.trim();
                if (text) {
                    chatClient.sendMessage(text);
                    input.value = '';
                }
            }
        });
    </script>
</body>
</html>

Common Errors & Debugging

Error: 403 Forbidden on Guest Token Request

Cause: The OAuth client does not have the webchat:guest scope.
Fix: Go to the Genesys Cloud Admin console, navigate to Applications > OAuth Clients, edit your client, and add the webchat:guest scope. Save the client and try again.

Error: WebSocket Connection Failed (401 Unauthorized)

Cause: The guestToken is invalid or expired.
Fix: Ensure you are using the correct guestToken from the /api/v2/webchat/guest/tokens response. Verify that the OAuth access token used to obtain the guest token is valid. Check the expiration time of the guest token and implement token refresh logic.

Error: WebSocket Connection Refused

Cause: You are connecting to the wrong host.
Fix: Ensure you are connecting to wss://webchat.mypurecloud.com, not wss://api.mypurecloud.com. The Guest WebSocket API is hosted on a separate domain.

Error: Message Not Delivered

Cause: The message payload structure is incorrect.
Fix: Ensure the message payload includes id, author, text, and timestamp. The author must be guest for incoming messages from the client. The id must be unique.

Official References