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-widgetpackage 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_credentialsgrant type. - Required Scopes: The client must have the
webchat:guestscope. 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
WebSocketobject. 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:
message: A new message from an agent or bot.typing: An agent is typing.status: The conversation status has changed (e.g.,connected,queued,closed).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.