Build a Custom Chat UI with the Genesys Cloud Guest WebSocket API
What You Will Build
- You will build a functional chat interface that connects directly to Genesys Cloud via WebSockets, bypassing the standard embedded widget.
- This implementation uses the Genesys Cloud Guest API (WebSocket) for real-time messaging and the REST API for session initialization.
- The tutorial covers JavaScript (frontend) and Python (backend proxy for OAuth) to handle authentication securely.
Prerequisites
- Genesys Cloud Account: An organization with Genesys Cloud Messaging enabled.
- OAuth Application: A “Confidential Client” application created in the Genesys Cloud Admin Portal.
- Required Scopes:
webchat:guest(for WebSocket connection)webchat:session:write(to create the chat session)webchat:session:read(to verify session status)
- Runtime: Node.js 18+ for the frontend logic demonstration, Python 3.9+ for the authentication backend.
- Dependencies:
- Frontend: No external libraries required; uses native
WebSocketandfetch. - Backend:
requestsfor Python.
- Frontend: No external libraries required; uses native
Authentication Setup
The Guest API requires a valid OAuth access token to establish the initial WebSocket handshake. Because the token contains sensitive credentials, you must never expose your OAuth Client Secret in frontend JavaScript. You will build a small backend endpoint to exchange your client credentials for a token.
Step 1: Create the Token Endpoint (Python)
Create a file named auth_server.py. This script exposes a simple HTTP endpoint that returns a short-lived access token.
import requests
from flask import Flask, jsonify
app = Flask(__name__)
# Replace these with your actual Genesys Cloud OAuth credentials
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
BASE_URL = "https://api.mypurecloud.com" # Use your specific region
def get_access_token():
"""
Retrieves an OAuth access token using the Client Credentials flow.
Scope: webchat:guest webchat:session:write
"""
url = f"{BASE_URL}/oauth/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "webchat:guest webchat:session:write"
}
try:
response = requests.post(url, headers=headers, data=data)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(f"OAuth Error: {e.response.text}")
return None
@app.route('/api/token', methods=['GET'])
def token_endpoint():
"""
Endpoint for the frontend to request a valid Genesys Cloud token.
In production, add rate limiting and caching here.
"""
token_data = get_access_token()
if token_data:
return jsonify({
"access_token": token_data["access_token"],
"expires_in": token_data["expires_in"]
})
return jsonify({"error": "Failed to retrieve token"}), 500
if __name__ == '__main__':
app.run(port=5000)
Run this server locally: python auth_server.py. It will listen on http://localhost:5000.
Implementation
Step 1: Initialize the Chat Session via REST
Before opening a WebSocket connection, you must create a chat session using the REST API. This returns a sessionId and a guestId, which are required for the WebSocket handshake.
Create a file named chat-ui.html. This file contains the HTML structure and the JavaScript logic.
<!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; }
#chat-container { width: 400px; border: 1px solid #ccc; height: 500px; display: flex; flex-direction: column; }
#messages { flex: 1; overflow-y: auto; padding: 10px; border-bottom: 1px solid #eee; }
.message { margin-bottom: 10px; padding: 8px; border-radius: 5px; }
.agent { background-color: #e0f7fa; align-self: flex-start; }
.guest { background-color: #fff3e0; align-self: flex-end; text-align: right; }
#input-area { display: flex; padding: 10px; }
#message-input { flex: 1; padding: 8px; margin-right: 10px; }
#send-btn { padding: 8px 16px; cursor: pointer; }
#status { font-size: 0.8em; color: #666; margin-bottom: 10px; }
</style>
</head>
<body>
<h2>Custom Genesys Chat</h2>
<div id="status">Disconnected</div>
<div id="chat-container">
<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 REGION = "mypurecloud.com"; // e.g., usw2.pure.cloud
const SUBDOMAIN = "your-subdomain"; // Your Genesys Cloud org subdomain
const WS_URL = `wss://api.${REGION}/api/v2/webchat/guest`;
const REST_URL = `https://api.${REGION}/api/v2/webchat/sessions`;
const TOKEN_ENDPOINT = "http://localhost:5000/api/token";
let accessToken = null;
let sessionId = null;
let guestId = null;
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');
// 1. Start the Connection Flow
async function startChat() {
try {
statusEl.textContent = "Authenticating...";
// Fetch token from our backend
const tokenResponse = await fetch(TOKEN_ENDPOINT);
const tokenData = await tokenResponse.json();
if (!tokenData.access_token) {
throw new Error("Failed to get access token");
}
accessToken = tokenData.access_token;
statusEl.textContent = "Creating Session...";
// Create Session via REST API
const sessionResponse = await fetch(REST_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
"capabilities": ["text"],
"routing": {
"skill": {
"name": "General Support" // Must match a skill in your org
}
}
})
});
if (!sessionResponse.ok) {
const errText = await sessionResponse.text();
throw new Error(`Session creation failed: ${errText}`);
}
const sessionData = await sessionResponse.json();
sessionId = sessionData.id;
guestId = sessionData.guestId;
statusEl.textContent = "Connecting to WebSocket...";
// Establish WebSocket Connection
connectWebSocket();
} catch (error) {
console.error(error);
statusEl.textContent = `Error: ${error.message}`;
}
}
// 2. WebSocket Connection Logic
function connectWebSocket() {
// The WebSocket URL must include the token and session details as query parameters
// Note: Genesys Cloud Guest API expects specific query params for handshake
const wsEndpoint = `${WS_URL}?access_token=${accessToken}&sessionId=${sessionId}&guestId=${guestId}`;
ws = new WebSocket(wsEndpoint);
ws.onopen = () => {
statusEl.textContent = "Connected";
inputEl.disabled = false;
sendBtn.disabled = false;
addMessage("system", "Connected to Genesys Cloud");
};
ws.onmessage = (event) => {
handleMessage(JSON.parse(event.data));
};
ws.onerror = (error) => {
statusEl.textContent = "WebSocket Error";
console.error("WebSocket Error:", error);
};
ws.onclose = (event) => {
statusEl.textContent = "Disconnected";
inputEl.disabled = true;
sendBtn.disabled = true;
addMessage("system", `Connection closed: ${event.reason}`);
};
}
// 3. Handle Incoming Messages
function handleMessage(msg) {
console.log("Received:", msg);
if (msg.type === "message") {
// This is a chat message from an agent or bot
const sender = msg.from === guestId ? "guest" : "agent";
addMessage(sender, msg.body);
}
else if (msg.type === "typing") {
// Handle typing indicators if desired
console.log(`${msg.from} is typing...`);
}
else if (msg.type === "error") {
addMessage("system", `Error: ${msg.body}`);
}
}
// 4. Send Messages
function sendMessage() {
const text = inputEl.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
// Construct the message payload
const payload = {
"type": "message",
"body": text,
"timestamp": new Date().toISOString()
};
ws.send(JSON.stringify(payload));
addMessage("guest", text);
inputEl.value = "";
}
// UI Helper
function addMessage(sender, text) {
const div = document.createElement('div');
div.className = `message ${sender}`;
div.textContent = text;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
// Event Listeners
sendBtn.addEventListener('click', sendMessage);
inputEl.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Initialize on load
startChat();
</script>
</body>
</html>
Step 2: Understanding the WebSocket Payload Structure
The Genesys Cloud Guest API uses a strict JSON structure for messages. When you send a message, the payload must include a unique id for each message to ensure delivery confirmation and ordering. The previous example simplified this for brevity, but production code should generate UUIDs.
Update the sendMessage function in the JavaScript above to include message IDs and handle acknowledgments:
// Helper to generate simple UUIDs
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function sendMessage() {
const text = inputEl.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
const payload = {
"id": generateUUID(), // Unique ID for this message
"type": "message",
"body": text,
"timestamp": new Date().toISOString(),
"from": guestId // Explicitly state the sender
};
ws.send(JSON.stringify(payload));
// Optimistic UI update
addMessage("guest", text);
inputEl.value = "";
}
Step 3: Handling Reconnection and Rate Limits
WebSockets can drop due to network instability or server maintenance. Genesys Cloud may also return a 429 status during the initial REST session creation if you are hitting rate limits.
Handling 429 Rate Limits in Python
Update the get_access_token function in auth_server.py to include basic retry logic:
import time
def get_access_token_with_retry(retries=3, delay=1):
url = f"{BASE_URL}/oauth/token"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "webchat:guest webchat:session:write"
}
for attempt in range(retries):
try:
response = requests.post(url, headers=headers, data=data)
if response.status_code == 429:
# Rate limited. Wait and retry.
wait_time = delay * (2 ** attempt)
print(f"Rate limited. Retrying in {wait_time} seconds...")
time.sleep(wait_time)
continue
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Request error: {e}")
time.sleep(delay)
return None
Handling WebSocket Disconnection in JavaScript
Add a reconnection mechanism to the ws.onclose handler. If the connection drops unexpectedly, attempt to reconnect after a short delay.
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_DELAY_MS = 3000;
ws.onclose = (event) => {
statusEl.textContent = "Disconnected";
inputEl.disabled = true;
sendBtn.disabled = true;
// If the close was not clean (code 1000), try to reconnect
if (event.code !== 1000 && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
statusEl.textContent = `Reconnecting... (${reconnectAttempts + 1})`;
setTimeout(() => {
reconnectAttempts++;
connectWebSocket();
}, RECONNECT_DELAY_MS);
} else {
addMessage("system", "Connection lost. Please refresh.");
}
};
// Reset reconnect attempts on successful open
ws.onopen = () => {
reconnectAttempts = 0;
statusEl.textContent = "Connected";
inputEl.disabled = false;
sendBtn.disabled = false;
};
Complete Working Example
Below is the consolidated chat-ui.html file. Ensure your auth_server.py is running on port 5000 before opening this file in a browser.
<!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: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #f5f5f5; padding: 20px; display: flex; justify-content: center; }
#chat-container { width: 400px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); display: flex; flex-direction: column; height: 600px; }
#header { padding: 15px; background: #0079bf; color: white; font-weight: bold; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center; }
#status { font-size: 0.8em; font-weight: normal; opacity: 0.9; }
#messages { flex: 1; overflow-y: auto; padding: 15px; background: #fafafa; }
.message { margin-bottom: 12px; padding: 10px 15px; border-radius: 18px; max-width: 80%; word-wrap: break-word; }
.agent { background-color: #e0f7fa; align-self: flex-start; border-bottom-left-radius: 4px; }
.guest { background-color: #0079bf; color: white; align-self: flex-end; border-bottom-right-radius: 4px; margin-left: auto; }
.system { background: none; color: #888; font-size: 0.8em; text-align: center; max-width: 100%; }
#input-area { padding: 15px; border-top: 1px solid #eee; display: flex; gap: 10px; }
#message-input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 20px; outline: none; }
#send-btn { padding: 10px 20px; background: #0079bf; color: white; border: none; border-radius: 20px; cursor: pointer; }
#send-btn:disabled { background: #ccc; cursor: not-allowed; }
</style>
</head>
<body>
<div id="chat-container">
<div id="header">
<span>Support Chat</span>
<span id="status">Connecting...</span>
</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 REGION = "mypurecloud.com";
const SUBDOMAIN = "your-subdomain";
const TOKEN_ENDPOINT = "http://localhost:5000/api/token";
let accessToken = null;
let sessionId = null;
let guestId = null;
let ws = null;
let reconnectAttempts = 0;
const statusEl = document.getElementById('status');
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
async function startChat() {
try {
statusEl.textContent = "Authenticating...";
const tokenResponse = await fetch(TOKEN_ENDPOINT);
const tokenData = await tokenResponse.json();
if (!tokenData.access_token) throw new Error("Token fetch failed");
accessToken = tokenData.access_token;
statusEl.textContent = "Creating Session...";
const sessionResponse = await fetch(`https://api.${REGION}/api/v2/webchat/sessions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
"capabilities": ["text"],
"routing": { "skill": { "name": "General Support" } }
})
});
if (!sessionResponse.ok) {
const err = await sessionResponse.text();
throw new Error(`Session Error: ${err}`);
}
const sessionData = await sessionResponse.json();
sessionId = sessionData.id;
guestId = sessionData.guestId;
connectWebSocket();
} catch (error) {
console.error(error);
statusEl.textContent = "Error";
addMessage("system", `Init Error: ${error.message}`);
}
}
function connectWebSocket() {
if (ws && ws.readyState === WebSocket.OPEN) ws.close();
const wsUrl = `wss://api.${REGION}/api/v2/webchat/guest?access_token=${accessToken}&sessionId=${sessionId}&guestId=${guestId}`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
reconnectAttempts = 0;
statusEl.textContent = "Online";
inputEl.disabled = false;
sendBtn.disabled = false;
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "message") {
const sender = msg.from === guestId ? "guest" : "agent";
addMessage(sender, msg.body);
}
};
ws.onerror = (e) => console.error("WS Error", e);
ws.onclose = (e) => {
inputEl.disabled = true;
sendBtn.disabled = true;
if (e.code !== 1000 && reconnectAttempts < 5) {
statusEl.textContent = "Reconnecting...";
setTimeout(() => {
reconnectAttempts++;
connectWebSocket();
}, 3000);
} else {
statusEl.textContent = "Offline";
addMessage("system", "Connection lost.");
}
};
}
function sendMessage() {
const text = inputEl.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
const payload = {
"id": crypto.randomUUID(),
"type": "message",
"body": text,
"timestamp": new Date().toISOString(),
"from": guestId
};
ws.send(JSON.stringify(payload));
addMessage("guest", text);
inputEl.value = "";
}
function addMessage(sender, text) {
const div = document.createElement('div');
div.className = `message ${sender}`;
div.textContent = text;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
sendBtn.addEventListener('click', sendMessage);
inputEl.addEventListener('keypress', (e) => e.key === 'Enter' && sendMessage());
startChat();
</script>
</body>
</html>
Common Errors & Debugging
Error: 401 Unauthorized on WebSocket Handshake
Cause: The access token provided in the WebSocket query parameter is expired or invalid.
Fix: Ensure your Python backend returns a fresh token. The Guest API token typically expires in 3600 seconds. If your session lasts longer, you must implement token refresh logic on the backend and reconnect the WebSocket with the new token.
Error: 403 Forbidden on Session Creation
Cause: The OAuth token lacks the webchat:session:write scope, or the specified Skill (“General Support” in the example) does not exist in your Genesys Cloud organization.
Fix:
- Check your OAuth App settings in the Admin Portal and ensure
webchat:session:writeis selected. - Verify the skill name in the
routingobject matches an active skill in your Genesys Cloud org.
Error: WebSocket Connection Refused
Cause: The region URL is incorrect. Genesys Cloud regions vary (e.g., usw2.pure.cloud, au02.pure.cloud).
Fix: Confirm your organization’s region URL. You can find this in the Genesys Cloud Admin Portal under Setup > Organization. Replace mypurecloud.com in the code with your specific region.
Error: Message Not Received by Agent
Cause: The guestId in the outgoing message payload does not match the guestId from the session creation response.
Fix: Ensure you are using the guestId returned by the POST /api/v2/webchat/sessions call in every outgoing WebSocket message.