How to Build a Custom Chat UI Using the WebSocket-Based Guest API
What You Will Build
- You will build a custom, low-latency web chat interface that connects directly to Genesys Cloud via the WebSocket Guest API, bypassing the standard Web Messaging Widget SDK.
- This implementation uses the native WebSocket protocol for real-time bidirectional communication and the REST API for session initialization.
- The tutorial covers JavaScript (ES6+) with vanilla DOM manipulation to ensure the logic is portable to any frontend framework.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth Client configured with the
webmessaging:guestscope. You must enable the “Web Messaging” capability in your Genesys Cloud organization settings. - API Version: Genesys Cloud API v2.
- Runtime: Any modern web browser with WebSocket support (Chrome, Firefox, Edge, Safari).
- Dependencies: None. This tutorial uses native
fetchandWebSocketAPIs. No external libraries like Socket.io or Axios are required, though you may use them in production. - Environment: You need a valid
client_idand a mechanism to obtain aguest_token(either via a backend proxy or client-side PKCE flow if supported by your security policy).
Authentication Setup
The WebSocket Guest API does not use standard OAuth Bearer tokens for the WebSocket connection itself. Instead, it uses a short-lived guest_token obtained via the REST API. This token acts as the handshake credential for the WebSocket upgrade.
The flow is:
- Call the REST endpoint to create a guest session.
- Receive the
guest_tokenandsession_id. - Use the
guest_tokenin the WebSocket query parameters to establish the connection.
Step 1: Obtain the Guest Token
You must authenticate with Genesys Cloud to get an OAuth access token first, then use that to create the guest session. For this tutorial, we assume you have a backend endpoint that returns the guest_token directly, or you are using a client-side flow where you already have an access_token with the webmessaging:guest scope.
Here is the code to create the guest session using the REST API.
/**
* Creates a new guest session and returns the guest token.
* @param {string} accessToken - The OAuth access token with webmessaging:guest scope.
* @param {string} organizationId - Your Genesys Cloud Organization ID.
* @returns {Promise<Object>} - Object containing guest_token and session_id.
*/
async function createGuestSession(accessToken, organizationId) {
const url = `https://api.us-east-1.mygen.com/api/v2/webmessaging/guest/sessions`;
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'X-Genesys-Organization': organizationId // Required for multi-tenant environments
};
const payload = {
"guestName": "Dev Advocate Demo",
"guestEmail": "demo@genesys.com",
"attributes": {
"customAttributeKey": "customAttributeValue"
}
};
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to create guest session: ${response.status} ${errorText}`);
}
const data = await response.json();
return {
guestToken: data.guest_token,
sessionId: data.session_id,
organizationId: data.organization_id
};
} catch (error) {
console.error("Error creating guest session:", error);
throw error;
}
}
OAuth Scope Required: webmessaging:guest
Expected Response:
{
"guest_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"organization_id": "gen-12345678-90ab-cdef-1234-567890abcdef",
"expires_in": 86400
}
Implementation
Step 2: Establish the WebSocket Connection
Once you have the guest_token, you can open a WebSocket connection. The Genesys Cloud WebSocket endpoint is distinct from the REST API endpoint.
Endpoint Format:
wss://webmessaging.us-east-1.mygen.com/api/v2/webmessaging/guest/ws?guest_token={GUEST_TOKEN}&organization_id={ORG_ID}
Note: Replace us-east-1 with your specific region (e.g., eu-west-1, ap-southeast-2).
/**
* Establishes a WebSocket connection to Genesys Cloud.
* @param {Object} sessionInfo - Object containing guestToken, organizationId, and sessionId.
* @param {Function} onMessage - Callback for incoming messages.
* @param {Function} onError - Callback for connection errors.
* @returns {WebSocket} - The active WebSocket instance.
*/
function connectWebSocket(sessionInfo, onMessage, onError) {
const { guestToken, organizationId } = sessionInfo;
const region = 'us-east-1'; // Change this to match your Genesys Cloud region
const wsUrl = `wss://webmessaging.${region}.mygen.com/api/v2/webmessaging/guest/ws?guest_token=${guestToken}&organization_id=${organizationId}`;
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log("WebSocket connection established.");
// Send a 'ping' to keep the connection alive if required by your infrastructure
// ws.send(JSON.stringify({ type: 'ping' }));
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
onMessage(message);
} catch (e) {
console.error("Failed to parse WebSocket message:", e);
}
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
onError(error);
};
ws.onclose = (event) => {
console.log(`WebSocket closed with code: ${event.code}, reason: ${event.reason}`);
// Implement reconnection logic here if necessary
};
return ws;
}
Step 3: Routing Messages and Sending Data
The WebSocket stream carries various message types. You must filter these based on the type field in the JSON payload. Common types include:
agent_typing: Agent is typing.agent_message: Agent sent a message.guest_message_delivered: Confirmation that your message was received.routing: Status updates about queue position or agent assignment.
Sending a Message
To send a message to the agent, you must send a JSON object with the type set to message and include the session_id.
/**
* Sends a chat message to the agent via WebSocket.
* @param {WebSocket} ws - The active WebSocket connection.
* @param {string} sessionId - The session ID from the guest creation step.
* @param {string} text - The message text.
*/
function sendMessage(ws, sessionId, text) {
if (ws.readyState !== WebSocket.OPEN) {
console.error("WebSocket is not open.");
return;
}
const messagePayload = {
"type": "message",
"session_id": sessionId,
"text": text
};
ws.send(JSON.stringify(messagePayload));
// Optimistically add to UI
addMessageToUI("user", text);
}
Processing Incoming Messages
You need a router to handle different message types.
function handleIncomingMessage(message) {
switch (message.type) {
case 'agent_message':
addMessageToUI("agent", message.text);
break;
case 'agent_typing':
showTypingIndicator(true);
break;
case 'agent_not_typing':
showTypingIndicator(false);
break;
case 'routing':
updateRoutingStatus(message);
break;
case 'guest_message_delivered':
// Optional: Update message status to 'sent'
break;
case 'error':
console.error("Genesys Cloud Error:", message.reason);
alert(`Error: ${message.reason}`);
break;
default:
console.warn("Unknown message type:", message.type, message);
}
}
Step 4: Handling Reconnection and Heartbeats
WebSockets can drop due to network instability. The Genesys Cloud Guest API supports a simple ping/pong mechanism, but it is often better to rely on the browser’s WebSocket lifecycle events and implement an exponential backoff reconnection strategy.
let wsInstance = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const BASE_DELAY = 1000; // 1 second
function initChat(sessionInfo) {
wsInstance = connectWebSocket(
sessionInfo,
handleIncomingMessage,
(error) => {
console.error("Connection error, attempting reconnect...", error);
attemptReconnect(sessionInfo);
}
);
}
function attemptReconnect(sessionInfo) {
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
alert("Max reconnect attempts reached. Please refresh the page.");
return;
}
const delay = BASE_DELAY * Math.pow(2, reconnectAttempts);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => {
reconnectAttempts++;
initChat(sessionInfo);
}, delay);
}
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 YOUR_ACCESS_TOKEN and YOUR_ORGANIZATION_ID with valid 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: 2rem auto; }
#chat-container { border: 1px solid #ccc; height: 400px; overflow-y: scroll; padding: 1rem; margin-bottom: 1rem; }
.message { margin-bottom: 0.5rem; padding: 0.5rem; border-radius: 4px; }
.user { background-color: #e3f2fd; align-self: flex-end; text-align: right; }
.agent { background-color: #f5f5f5; }
.typing { font-style: italic; color: #888; font-size: 0.8rem; }
#input-area { display: flex; gap: 0.5rem; }
input { flex-grow: 1; padding: 0.5rem; }
button { padding: 0.5rem 1rem; background-color: #007bff; color: white; border: none; cursor: pointer; }
button:disabled { background-color: #ccc; }
#status { margin-bottom: 1rem; font-weight: bold; }
</style>
</head>
<body>
<h1>Custom Genesys Chat</h1>
<div id="status">Connecting...</div>
<div id="chat-container"></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 ACCESS_TOKEN = "YOUR_ACCESS_TOKEN_HERE"; // Replace with valid token
const ORGANIZATION_ID = "YOUR_ORGANIZATION_ID_HERE"; // Replace with valid Org ID
const REGION = "us-east-1"; // Match your region
let wsInstance = null;
let sessionInfo = null;
let reconnectAttempts = 0;
// UI Elements
const chatContainer = document.getElementById('chat-container');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const statusDiv = document.getElementById('status');
// 1. Initialize Session
async function initSession() {
try {
statusDiv.textContent = "Creating session...";
sessionInfo = await createGuestSession(ACCESS_TOKEN, ORGANIZATION_ID);
statusDiv.textContent = "Session created. Connecting to WebSocket...";
initChat();
} catch (error) {
statusDiv.textContent = `Error: ${error.message}`;
console.error(error);
}
}
// 2. Create Guest Session via REST
async function createGuestSession(accessToken, organizationId) {
const url = `https://api.${REGION}.mygen.com/api/v2/webmessaging/guest/sessions`;
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'X-Genesys-Organization': organizationId
};
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify({
"guestName": "Web Dev",
"guestEmail": "dev@example.com"
})
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
return {
guestToken: data.guest_token,
organizationId: data.organization_id,
sessionId: data.session_id
};
}
// 3. Connect WebSocket
function initChat() {
const wsUrl = `wss://webmessaging.${REGION}.mygen.com/api/v2/webmessaging/guest/ws?guest_token=${sessionInfo.guestToken}&organization_id=${sessionInfo.organizationId}`;
wsInstance = new WebSocket(wsUrl);
wsInstance.onopen = () => {
statusDiv.textContent = "Connected";
enableInput();
addMessageToUI("system", "Chat connected. You can start typing.");
};
wsInstance.onmessage = (event) => {
const message = JSON.parse(event.data);
handleIncomingMessage(message);
};
wsInstance.onclose = (event) => {
statusDiv.textContent = "Disconnected";
disableInput();
if (event.code !== 1000) { // Not a clean close
attemptReconnect();
}
};
wsInstance.onerror = (error) => {
console.error("WebSocket error", error);
};
}
// 4. Handle Messages
function handleIncomingMessage(message) {
switch (message.type) {
case 'agent_message':
addMessageToUI("agent", message.text);
break;
case 'agent_typing':
updateTypingIndicator(true);
break;
case 'agent_not_typing':
updateTypingIndicator(false);
break;
case 'routing':
if (message.status === 'queued') {
addMessageToUI("system", "You are in the queue.");
} else if (message.status === 'connected') {
addMessageToUI("system", "Connected to an agent.");
}
break;
default:
console.log("Unhandled message type:", message.type);
}
}
// 5. Send Message
function sendMessage() {
const text = messageInput.value.trim();
if (!text || !wsInstance || wsInstance.readyState !== WebSocket.OPEN) return;
const payload = {
"type": "message",
"session_id": sessionInfo.sessionId,
"text": text
};
wsInstance.send(JSON.stringify(payload));
addMessageToUI("user", text);
messageInput.value = "";
}
// UI Helpers
function addMessageToUI(sender, text) {
const div = document.createElement('div');
div.className = `message ${sender}`;
div.textContent = text;
chatContainer.appendChild(div);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function updateTypingIndicator(isTyping) {
let typingDiv = document.getElementById('typing-indicator');
if (isTyping) {
if (!typingDiv) {
typingDiv = document.createElement('div');
typingDiv.id = 'typing-indicator';
typingDiv.className = 'typing';
typingDiv.textContent = "Agent is typing...";
chatContainer.appendChild(typingDiv);
}
chatContainer.scrollTop = chatContainer.scrollHeight;
} else {
if (typingDiv) typingDiv.remove();
}
}
function enableInput() {
messageInput.disabled = false;
sendBtn.disabled = false;
messageInput.focus();
}
function disableInput() {
messageInput.disabled = true;
sendBtn.disabled = true;
}
function attemptReconnect() {
if (reconnectAttempts > 5) {
statusDiv.textContent = "Failed to reconnect.";
return;
}
reconnectAttempts++;
setTimeout(initChat, 2000 * reconnectAttempts);
}
// Event Listeners
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Start
initSession();
</script>
</body>
</html>
Common Errors & Debugging
Error: 401 Unauthorized on REST Call
- Cause: The
access_tokenprovided tocreateGuestSessionis invalid, expired, or lacks thewebmessaging:guestscope. - Fix: Verify the token in a JWT decoder. Ensure the OAuth client used to generate the token has the
webmessaging:guestscope enabled in the Genesys Cloud Admin > Security > OAuth Clients.
Error: WebSocket Connection Refused / 403 on WS Upgrade
- Cause: The
guest_tokenis invalid, expired, or theorganization_idin the WebSocket URL does not match the organization associated with the token. - Fix: Check the expiration time of the
guest_token. Ensure you are passing the correctorganization_idfrom the REST response into the WebSocket URL query parameters.
Error: session_id Mismatch
- Cause: You are sending messages with a
session_idthat does not match the one returned during thePOST /guest/sessionscall. - Fix: Ensure you capture the
session_idfrom the initial REST response and reuse it exactly for all subsequent WebSocket messages. Do not generate a new UUID client-side.
Error: Messages Not Appearing
- Cause: The routing configuration in Genesys Cloud does not have a skill or queue assigned to the web messaging channel, or the agent is not available.
- Fix: Check the
routingmessage types in the console. If you receiveroutingwithstatus: 'queued'but noconnectedevent, check your Genesys Cloud Workforce Management or Routing configuration to ensure agents are online and assigned to the correct skill.