Building a Custom Chat UI with Genesys Cloud Guest WebSocket API
What You Will Build
- A fully functional, custom HTML/JavaScript chat interface that connects directly to Genesys Cloud via WebSocket.
- This solution bypasses the standard Genesys Web Messaging Widget, giving you complete control over the DOM, styling, and user experience.
- The implementation uses vanilla JavaScript and the native WebSocket API, requiring no external SDKs for the messaging layer.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth client with the
webchat:guestscope. This scope is critical for establishing the guest session. - Genesys Cloud Instance: A valid subdomain (e.g.,
mypurecloud.com) and a configured Web Messaging flow or IVR that accepts WebSocket connections. - Runtime Environment: Any modern web browser that supports ES6+ JavaScript and the WebSocket API.
- Dependencies: None. This tutorial uses vanilla JavaScript. If you prefer a library,
axioscan be used for the initial REST calls, butfetchis sufficient.
Authentication Setup
The Genesys Cloud Guest WebSocket API does not use standard Bearer tokens for the WebSocket connection itself. Instead, it uses a two-step process:
- REST Call: Obtain a temporary guest token via the
/api/v2/webchatendpoint. - WebSocket Handshake: Use that token as a query parameter when establishing the WebSocket connection.
The REST endpoint requires a valid OAuth Bearer token generated from your OAuth client credentials.
Step 1: Obtain OAuth Bearer Token
First, you must authenticate your application to Genesys Cloud to get the permission to create a guest session.
// Configuration
const CLIENT_ID = 'your-client-id';
const CLIENT_SECRET = 'your-client-secret';
const DOMAIN = 'https://mypurecloud.com';
/**
* Fetches an OAuth Bearer token using Client Credentials flow.
* @returns {Promise<string>} The Bearer token.
*/
async function getBearerToken() {
const url = `${DOMAIN}/oauth/token`;
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
params.append('client_id', CLIENT_ID);
params.append('client_secret', CLIENT_SECRET);
params.append('scope', 'webchat:guest'); // Essential scope
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`OAuth Error ${response.status}: ${errorData.error_description}`);
}
const data = await response.json();
return data.access_token;
} catch (error) {
console.error('Failed to obtain OAuth token:', error);
throw error;
}
}
Step 2: Create Guest Session
With the Bearer token, you request a guest session. This returns a guestId and a token that is specific to this chat session.
/**
* Creates a Genesys Cloud Webchat Guest Session.
* @param {string} bearerToken - The OAuth token.
* @returns {Promise<Object>} The guest session object containing id and token.
*/
async function createGuestSession(bearerToken) {
const url = `${DOMAIN}/api/v2/webchat`;
// Optional: You can send metadata here which will be attached to the transcript
const payload = {
"type": "webchat",
"data": {
"name": "John Doe",
"email": "john.doe@example.com"
},
"metadata": {
"customField1": "value1"
}
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${bearerToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Guest Session Error ${response.status}: ${errorData.message}`);
}
const session = await response.json();
return {
guestId: session.id,
guestToken: session.token,
conversationId: session.conversationId // Useful for tracking
};
} catch (error) {
console.error('Failed to create guest session:', error);
throw error;
}
}
Implementation
Step 1: Establishing the WebSocket Connection
The WebSocket URL is constructed dynamically based on your Genesys Cloud subdomain. The protocol changes depending on whether you are using HTTP or HTTPS (ws vs wss).
Critical Note: The guestToken obtained in the previous step must be passed as a query parameter named token.
/**
* Establishes the WebSocket connection to Genesys Cloud.
* @param {Object} sessionData - The session object from createGuestSession.
* @returns {WebSocket} The active WebSocket instance.
*/
function connectWebSocket(sessionData) {
// Determine protocol based on the domain
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
// The host is typically webchat.mypurecloud.com or similar,
// but for Genesys Cloud Webchat API, we use the standard API host pattern.
// Note: The actual WebSocket host is often 'webchat.{subdomain}.com'
const host = 'webchat.mypurecloud.com';
// Construct URL with the guest token
const wsUrl = `${protocol}://${host}/api/v2/webchat?token=${sessionData.guestToken}`;
const ws = new WebSocket(wsUrl);
// Handle Connection Open
ws.onopen = () => {
console.log('WebSocket connected to Genesys Cloud');
updateUIStatus('Connected');
// Send initial presence or metadata if required by your flow
// Usually, the connection itself triggers the IVR/Flow
};
// Handle Incoming Messages
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleIncomingMessage(message);
} catch (e) {
console.error('Failed to parse incoming message:', e);
}
};
// Handle Errors
ws.onerror = (error) => {
console.error('WebSocket error:', error);
updateUIStatus('Connection Error');
};
// Handle Close
ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
updateUIStatus('Disconnected');
// Optional: Implement reconnection logic here if needed
};
return ws;
}
Step 2: Handling Message Types
Genesys Cloud sends various message types over the WebSocket. You must inspect the type field to determine how to render the message. Common types include:
message: Standard text from a bot or agent.typing: Indicates the other party is typing.presence: Updates on agent availability.error: System errors.
/**
* Processes incoming messages from the WebSocket.
* @param {Object} message - The parsed JSON message.
*/
function handleIncomingMessage(message) {
const { type, text, sender, timestamp } = message;
switch (type) {
case 'message':
// Render the message in the UI
renderMessage(text, sender === 'agent' ? 'agent' : 'bot', timestamp);
break;
case 'typing':
// Show a typing indicator
showTypingIndicator(sender === 'agent');
break;
case 'presence':
// Update UI to show if an agent is connected
if (message.connected) {
updateUIStatus('Agent Connected');
} else {
updateUIStatus('Waiting for Agent');
}
break;
case 'error':
console.error('Genesys Cloud Error:', message.reason);
alert(`System Error: ${message.reason}`);
break;
default:
console.warn('Unknown message type:', type, message);
}
}
Step 3: Sending Messages
To send a message, you construct a JSON payload with type: "message" and text. This payload must be stringified before sending via the WebSocket.
/**
* Sends a text message to the chat session.
* @param {WebSocket} ws - The active WebSocket connection.
* @param {string} text - The message content.
*/
function sendMessage(ws, text) {
if (ws.readyState !== WebSocket.OPEN) {
alert('Not connected to Genesys Cloud');
return;
}
const payload = {
"type": "message",
"text": text
};
try {
ws.send(JSON.stringify(payload));
// Optionally add the message to the UI immediately for optimistic UI
renderMessage(text, 'guest', new Date().toISOString());
} catch (error) {
console.error('Failed to send message:', error);
alert('Failed to send message');
}
}
Complete Working Example
This is a single-file HTML/JS solution. Save this as index.html and open it in a browser. You must replace the CLIENT_ID, CLIENT_SECRET, and DOMAIN variables.
<!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; background: #f4f4f4; }
#chat-container { max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); height: 500px; display: flex; flex-direction: column; }
#messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 10px; }
.message { max-width: 70%; padding: 10px; border-radius: 10px; }
.message.agent { background: #e0e0e0; align-self: flex-start; }
.message.bot { background: #e0e0e0; align-self: flex-start; }
.message.guest { background: #007bff; color: white; align-self: flex-end; }
#input-area { padding: 20px; border-top: 1px solid #eee; display: flex; gap: 10px; }
#message-input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
#send-btn { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
#send-btn:disabled { background: #ccc; }
#status { padding: 10px; background: #333; color: white; text-align: center; font-size: 12px; }
</style>
</head>
<body>
<div id="chat-container">
<div id="status">Connecting...</div>
<div id="messages"></div>
<div id="input-area">
<input type="text" id="message-input" placeholder="Type a message..." disabled>
<button id="send-btn" disabled>Send</button>
</div>
</div>
<script>
// --- CONFIGURATION ---
const CLIENT_ID = 'YOUR_CLIENT_ID';
const CLIENT_SECRET = 'YOUR_CLIENT_SECRET';
const DOMAIN = 'https://YOUR_SUBDOMAIN.mypurecloud.com';
// --- STATE ---
let ws = null;
const statusEl = document.getElementById('status');
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
// --- OAUTH & SESSION ---
async function initChat() {
try {
statusEl.textContent = 'Authenticating...';
const bearerToken = await getBearerToken();
statusEl.textContent = 'Creating Session...';
const session = await createGuestSession(bearerToken);
statusEl.textContent = 'Connecting WebSocket...';
ws = connectWebSocket(session);
} catch (error) {
statusEl.textContent = 'Failed to connect: ' + error.message;
console.error(error);
}
}
async function getBearerToken() {
const url = `${DOMAIN}/oauth/token`;
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'webchat:guest'
});
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
if (!response.ok) throw new Error(await response.text());
const data = await response.json();
return data.access_token;
}
async function createGuestSession(bearerToken) {
const url = `${DOMAIN}/api/v2/webchat`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${bearerToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: "webchat",
data: { name: "Guest User" }
})
});
if (!response.ok) throw new Error(await response.text());
return await response.json();
}
function connectWebSocket(sessionData) {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
// Note: Ensure you use the correct webchat host for your region
const host = 'webchat.mypurecloud.com';
const wsUrl = `${protocol}://${host}/api/v2/webchat?token=${sessionData.guestToken}`;
const socket = new WebSocket(wsUrl);
socket.onopen = () => {
statusEl.textContent = 'Connected';
inputEl.disabled = false;
sendBtn.disabled = false;
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'message') {
renderMessage(msg.text, msg.sender === 'agent' ? 'agent' : 'bot');
}
};
socket.onerror = (err) => {
statusEl.textContent = 'WebSocket Error';
console.error(err);
};
socket.onclose = () => {
statusEl.textContent = 'Disconnected';
inputEl.disabled = true;
sendBtn.disabled = true;
};
return socket;
}
// --- UI HELPERS ---
function renderMessage(text, sender) {
const div = document.createElement('div');
div.className = `message ${sender}`;
div.textContent = text;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function handleSend() {
const text = inputEl.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ type: "message", text: text }));
renderMessage(text, 'guest');
inputEl.value = '';
}
sendBtn.addEventListener('click', handleSend);
inputEl.addEventListener('keypress', (e) => {
if (e.key === 'Enter') handleSend();
});
// Start the process
initChat();
</script>
</body>
</html>
Common Errors & Debugging
Error: 401 Unauthorized on /api/v2/webchat
- Cause: The OAuth Bearer token is invalid, expired, or lacks the
webchat:guestscope. - Fix: Verify your
CLIENT_IDandCLIENT_SECRETare correct. Ensure the OAuth client in Genesys Cloud Admin has thewebchat:guestscope enabled. Check the network tab to ensure theAuthorization: Bearer <token>header is present in the POST request to/api/v2/webchat.
Error: WebSocket Connection Failed (403 Forbidden)
- Cause: The
guestTokenpassed in the WebSocket URL is invalid or expired. Guest tokens are short-lived. - Fix: Ensure you are using the
tokenfield from the JSON response of the/api/v2/webchatcall. Do not reuse a token from a previous session. If the connection fails immediately, check that thetokenquery parameter is correctly appended to the WebSocket URL.
Error: WebSocket Host Resolution Failure
- Cause: The WebSocket host (
webchat.mypurecloud.com) may differ based on your Genesys Cloud region (e.g.,webchat.euw1.pure.cloud). - Fix: Consult the Genesys Cloud API documentation for your specific region’s WebSocket endpoint. The REST API host and WebSocket host are often different. For example, if your API host is
api.pure.cloud, your WebSocket host might bewebchat.pure.cloud.
Error: Messages Not Appearing
- Cause: The Genesys Cloud Web Messaging Flow is not configured to respond, or the bot is waiting for specific input.
- Fix: Check the Genesys Cloud Admin console under “Engagement > Web Messaging”. Ensure there is an active flow assigned to the website or that the default flow is configured to send a greeting message upon connection.