Build a Custom Chat UI with Genesys Cloud Guest WebSocket API
What You Will Build
- A fully functional chat interface that connects directly to Genesys Cloud using the WebSocket-based Guest API, bypassing the standard Web Messaging widget.
- Implementation of the three-phase handshake: REST-based login, WebSocket upgrade, and message exchange.
- A working example in JavaScript (Vanilla ES6) that handles connection lifecycle, message rendering, and error states.
Prerequisites
- Platform: Genesys Cloud CX
- API Version: v2 (Guest Messaging API)
- Language: JavaScript (ES6+)
- Dependencies: None (uses native
WebSocketandfetch). - Required Scopes: None for the client-side implementation (the Guest API uses token-less authentication via a generated login token). However, your backend application must have the
guest:loginscope if you are generating login tokens server-side, or you will use the public endpoint directly.
Authentication Setup
The Guest API differs from standard Genesys Cloud APIs because it does not use OAuth2 Bearer tokens for the end-user session. Instead, it uses a two-step authentication process:
- REST Login: The client sends a
POSTto/api/v2/guest/message/loginsto obtain a session token. - WebSocket Upgrade: The client opens a WebSocket connection using that token to begin real-time communication.
This tutorial assumes you are connecting directly from a browser. In production, you should proxy the initial login request through your own backend to hide your Genesys Cloud siteName and region configuration from the client, but the WebSocket connection itself must originate from the client.
Step 1: The Login Request
To start a chat session, you must send a login request. This endpoint requires your Genesys Cloud organization’s siteName (e.g., mypurecloud.com) and region (e.g., us-east-1).
Endpoint: POST https://{siteName}.mypurecloud.com/api/v2/guest/message/logins
Request Body:
{
"name": "John Doe",
"email": "john.doe@example.com",
"language": "en-US",
"routing": {
"queueId": "your-queue-id-here"
}
}
JavaScript Implementation:
const CONFIG = {
siteName: 'your-site-name', // e.g., 'acme'
region: 'us-east-1', // e.g., 'us-east-1', 'eu-west-1'
queueId: '12345678-1234-1234-1234-123456789012' // Target Queue ID
};
async function initiateGuestLogin() {
const loginUrl = `https://${CONFIG.siteName}.mypurecloud.com/api/v2/guest/message/logins`;
const payload = {
name: "Test User",
email: "test@example.com",
language: "en-US",
routing: {
queueId: CONFIG.queueId
}
};
try {
const response = await fetch(loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Login failed: ${response.status} - ${errorText}`);
}
const loginData = await response.json();
// Critical fields for the next step
return {
sessionId: loginData.sessionId,
token: loginData.token,
websocketUrl: loginData.websocketUrl // Genesys provides the exact WS endpoint
};
} catch (error) {
console.error("Guest Login Error:", error);
throw error;
}
}
Expected Response:
The response contains the sessionId and token required for the WebSocket handshake. It also includes websocketUrl, which is the exact endpoint to connect to. Do not construct this URL manually; use the one provided in the response.
Implementation
Step 2: Establishing the WebSocket Connection
Once you have the token and websocketUrl, you must open a WebSocket connection. Genesys Cloud expects specific query parameters or headers depending on the SDK, but for raw WebSocket connections, the URL provided in the login response is pre-configured with the necessary authentication parameters.
Important: The WebSocket connection is persistent. You must handle onopen, onmessage, onclose, and onerror.
class GuestChatClient {
constructor(sessionData) {
this.sessionId = sessionData.sessionId;
this.token = sessionData.token;
this.wsUrl = sessionData.websocketUrl;
this.ws = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
connect() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
console.log("Already connected");
return;
}
console.log(`Connecting to WebSocket: ${this.wsUrl}`);
// Note: The wsUrl from the login response usually includes the token as a query param
// or requires it in the protocol. We use the URL directly as provided.
this.ws = new WebSocket(this.wsUrl);
this.ws.onopen = () => {
console.log("WebSocket Connected");
this.isConnected = true;
this.reconnectAttempts = 0;
this.notifyStatus("Connected");
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onclose = (event) => {
console.log(`WebSocket Closed: Code ${event.code}, Reason: ${event.reason}`);
this.isConnected = false;
this.handleDisconnect(event);
};
this.ws.onerror = (error) => {
console.error("WebSocket Error:", error);
this.notifyStatus("Connection Error");
};
}
handleMessage(rawData) {
try {
const message = JSON.parse(rawData);
console.log("Received Message:", message);
// Handle specific message types
switch (message.type) {
case 'conversation':
this.handleConversationUpdate(message);
break;
case 'message':
this.handleChatMessage(message);
break;
case 'error':
this.handleApiError(message);
break;
default:
console.log("Unknown message type:", message.type);
}
} catch (e) {
console.error("Failed to parse WebSocket message:", e);
}
}
handleChatMessage(msg) {
// msg.payload contains the actual chat content
if (msg.payload && msg.payload.content) {
this.renderMessage({
sender: msg.payload.from === this.sessionId ? 'me' : 'agent',
text: msg.payload.content,
timestamp: msg.payload.timestamp
});
}
}
handleConversationUpdate(msg) {
// Handles events like 'agentJoined', 'queuePosition', etc.
if (msg.payload && msg.payload.event) {
const event = msg.payload.event;
if (event === 'agentJoined') {
this.notifyStatus("Agent Connected");
} else if (event === 'queuePosition') {
this.notifyStatus(`Position in queue: ${msg.payload.position}`);
}
}
}
handleApiError(msg) {
console.error("Genesys API Error:", msg.payload);
this.notifyStatus("Service Error");
}
handleDisconnect(event) {
// Simple reconnection logic
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.pow(2, this.reconnectAttempts) * 1000; // Exponential backoff
console.log(`Reconnecting in ${delay}ms... (Attempt ${this.reconnectAttempts})`);
setTimeout(() => this.connect(), delay);
} else {
this.notifyStatus("Max Reconnect Attempts Reached");
}
}
sendMessage(text) {
if (!this.isConnected) {
console.error("Cannot send message: Not connected");
return;
}
const messagePayload = {
type: 'message',
payload: {
content: text,
from: this.sessionId,
timestamp: new Date().toISOString()
}
};
this.ws.send(JSON.stringify(messagePayload));
}
disconnect() {
if (this.ws) {
this.ws.close(1000, "Client disconnecting");
}
}
// UI Integration Hooks
renderMessage(msg) {
const chatContainer = document.getElementById('chat-messages');
if (!chatContainer) return;
const div = document.createElement('div');
div.className = `message ${msg.sender}`;
div.innerHTML = `<strong>${msg.sender === 'me' ? 'You' : 'Agent'}:</strong> ${msg.text}`;
chatContainer.appendChild(div);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
notifyStatus(status) {
const statusEl = document.getElementById('connection-status');
if (statusEl) {
statusEl.textContent = status;
}
}
}
Step 3: Processing Results and Lifecycle Management
The Guest API sends various event types over the WebSocket. You must filter these to update your UI correctly. The most common types are:
message: A text chat message from the agent or system.conversation: Metadata updates (e.g., agent joined, transfer, hold).typing: Indicates the other party is typing (if supported by the queue configuration).
Error Handling Strategy:
WebSocket errors can be silent. Always implement a heartbeat or ping mechanism if the connection is idle for long periods. Genesys Cloud supports standard WebSocket ping/pong frames, but browser implementations vary. The code above includes a reconnection loop, which is the most robust way to handle network drops.
Sending Messages:
When sending a message, you must structure the JSON payload exactly as expected by the Genesys Cloud gateway. The type must be message, and the payload must include content, from, and timestamp.
Complete Working Example
This HTML file contains the complete logic. Save it as index.html and open it in a browser. You must replace CONFIG values with your actual Genesys Cloud details.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Genesys Cloud Chat</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
#chat-container { border: 1px solid #ccc; height: 400px; overflow-y: scroll; padding: 1rem; margin-bottom: 1rem; background: #f9f9f9; }
.message { margin: 0.5rem 0; padding: 0.5rem; border-radius: 4px; }
.message.me { background: #e3f2fd; text-align: right; }
.message.agent { background: #fff3e0; text-align: left; }
#input-area { display: flex; gap: 0.5rem; }
#message-input { flex: 1; padding: 0.5rem; }
button { padding: 0.5rem 1rem; cursor: pointer; }
#connection-status { font-weight: bold; color: #666; margin-bottom: 1rem; }
.error { color: red; }
</style>
</head>
<body>
<h1>Custom Chat UI</h1>
<div id="connection-status">Disconnected</div>
<div id="chat-container">
<div id="chat-messages"></div>
</div>
<div id="input-area">
<input type="text" id="message-input" placeholder="Type a message..." disabled>
<button id="send-btn" disabled>Send</button>
<button id="start-btn">Start Chat</button>
</div>
<script>
const CONFIG = {
siteName: 'your-site-name', // REPLACE THIS
region: 'us-east-1', // REPLACE THIS
queueId: 'your-queue-id' // REPLACE THIS
};
let chatClient = null;
const startBtn = document.getElementById('start-btn');
const sendBtn = document.getElementById('send-btn');
const messageInput = document.getElementById('message-input');
startBtn.addEventListener('click', async () => {
try {
startBtn.disabled = true;
startBtn.textContent = "Connecting...";
// Step 1: Login
const sessionData = await initiateGuestLogin();
// Step 2: Initialize Client
chatClient = new GuestChatClient(sessionData);
chatClient.connect();
// Enable UI
messageInput.disabled = false;
sendBtn.disabled = false;
startBtn.textContent = "Connected";
} catch (err) {
console.error(err);
document.getElementById('connection-status').textContent = "Login Failed";
document.getElementById('connection-status').classList.add('error');
startBtn.disabled = false;
startBtn.textContent = "Retry";
}
});
sendBtn.addEventListener('click', () => {
const text = messageInput.value.trim();
if (text && chatClient) {
chatClient.sendMessage(text);
// Render local message immediately for better UX
chatClient.renderMessage({
sender: 'me',
text: text,
timestamp: new Date().toISOString()
});
messageInput.value = '';
}
});
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendBtn.click();
});
// --- Functions from Previous Sections ---
async function initiateGuestLogin() {
const loginUrl = `https://${CONFIG.siteName}.mypurecloud.com/api/v2/guest/message/logins`;
const payload = {
name: "Test User",
email: "test@example.com",
language: "en-US",
routing: {
queueId: CONFIG.queueId
}
};
const response = await fetch(loginUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Login failed: ${response.status} - ${errorText}`);
}
return await response.json();
}
class GuestChatClient {
constructor(sessionData) {
this.sessionId = sessionData.sessionId;
this.token = sessionData.token;
this.wsUrl = sessionData.websocketUrl;
this.ws = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
connect() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
this.ws = new WebSocket(this.wsUrl);
this.ws.onopen = () => {
this.isConnected = true;
this.reconnectAttempts = 0;
this.notifyStatus("Connected");
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onclose = (event) => {
this.isConnected = false;
this.handleDisconnect(event);
};
this.ws.onerror = (error) => {
this.notifyStatus("Connection Error");
};
}
handleMessage(rawData) {
try {
const message = JSON.parse(rawData);
if (message.type === 'message' && message.payload && message.payload.content) {
// Only render if not from me (to avoid duplicates if we render locally)
if (message.payload.from !== this.sessionId) {
this.renderMessage({
sender: 'agent',
text: message.payload.content,
timestamp: message.payload.timestamp
});
}
}
} catch (e) {
console.error("Parse error", e);
}
}
handleDisconnect(event) {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => this.connect(), Math.pow(2, this.reconnectAttempts) * 1000);
} else {
this.notifyStatus("Disconnected permanently");
}
}
sendMessage(text) {
if (!this.isConnected) return;
const messagePayload = {
type: 'message',
payload: {
content: text,
from: this.sessionId,
timestamp: new Date().toISOString()
}
};
this.ws.send(JSON.stringify(messagePayload));
}
renderMessage(msg) {
const chatContainer = document.getElementById('chat-messages');
const div = document.createElement('div');
div.className = `message ${msg.sender}`;
div.innerHTML = `<strong>${msg.sender === 'me' ? 'You' : 'Agent'}:</strong> ${msg.text}`;
chatContainer.appendChild(div);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
notifyStatus(status) {
const statusEl = document.getElementById('connection-status');
if (statusEl) statusEl.textContent = status;
}
}
</script>
</body>
</html>
Common Errors & Debugging
Error: 401 Unauthorized on Login
Cause: The siteName or queueId is incorrect, or the queue is not configured to accept guest messages.
Fix:
- Verify
siteNamematches your Genesys Cloud URL (e.g., if your URL isacme.mypurecloud.com,siteNameisacme). - Verify
queueIdis a valid Queue ID in your organization. - Ensure the Queue has “Web Messaging” enabled in the Genesys Cloud Admin console.
Error: WebSocket Connection Refused
Cause: The websocketUrl obtained from the login response is malformed or the token has expired before the WebSocket handshake.
Fix:
- Log the
websocketUrlimmediately after login. It should look likewss://websocket.mypurecloud.com/.... - Ensure you do not delay the
connect()call. The login token expires quickly (usually within 30-60 seconds). - Check browser console for CORS errors. The login endpoint supports CORS, but ensure your domain is allowed if you have strict CSP settings.
Error: Messages Not Appearing
Cause: The client is sending messages, but the server is not responding, or the client is filtering out messages incorrectly.
Fix:
- Check the
onmessagehandler. Ensure you are parsingevent.dataas JSON. - Verify that the
fromfield in the incoming message matches thesessionIdof the agent, not the guest. - Check if the agent has accepted the chat. The guest will see
queuePositionevents until an agent joins. Messages sent before an agent joins are queued by Genesys and delivered to the agent upon join.