Sending Typing Indicators and Read Receipts via the Genesys Cloud Web Messaging Guest API
What You Will Build
- This code sends real-time typing status updates and read receipts from a client-side web application to a Genesys Cloud Web Messaging session.
- It utilizes the Genesys Cloud Web Messaging Guest API endpoints (
/api/v2/webchat/messages) and the WebSocket message protocol. - The implementation is demonstrated in JavaScript (ES6+) for a browser environment, as this is the primary runtime for Web Messaging guests.
Prerequisites
- Platform: Genesys Cloud CX
- API Endpoint:
POST /api/v2/webchat/messages(for initial setup) and the established WebSocket connection for subsequent events. - Language: JavaScript (Node.js or Browser Environment). This tutorial assumes a browser context using the
fetchAPI and nativeWebSocket. - Dependencies: None. This uses standard browser APIs. No external npm packages are required for the core logic, though
uuidor similar libraries are helpful for generating client-side message IDs. - Concept: Understanding that Web Messaging in Genesys Cloud operates over a persistent WebSocket connection after an initial HTTP handshake. Typing indicators and read receipts are not separate REST endpoints; they are JSON payloads sent over the existing WebSocket channel.
Authentication Setup
Web Messaging does not use OAuth tokens for the guest user. Instead, it uses a session-based authentication model.
- Initiate Session: You must first call the
POST /api/v2/webchat/messagesendpoint to create a new Web Messaging session. - Receive Connection Info: The response contains a
connectionUrl(WebSocket endpoint) and aconnectionToken. - Establish WebSocket: Connect to the
connectionUrlusing theconnectionTokenin the query parameters or header (depending on your SDK implementation, but typically passed in the initial WebSocket handshake or as a message payload).
Note on Scopes: The initial HTTP call requires no OAuth scope if you are using a public Web Messaging widget configuration. If you are building a custom backend proxy, you might use an OAuth token with the webchat:guest scope, but for client-side guest interactions, the session token is the sole credential.
Implementation
Step 1: Establish the Web Messaging Session
Before sending typing indicators, you must have an active WebSocket connection. The following code initiates the session and upgrades to a WebSocket connection.
// Configuration
const GENESYS_ORG_ID = "your-org-id"; // e.g., "12345678-1234-1234-1234-123456789012"
const WEB_CHAT_CONFIG_ID = "your-webchat-config-id"; // Retrieved from Genesys Admin UI
/**
* Initiates a Web Messaging session and establishes the WebSocket connection.
* @returns {Promise<Object>} An object containing the WebSocket connection and session details.
*/
async function initWebMessagingSession() {
const baseUrl = `https://${GENESYS_ORG_ID}.mypurecloud.com/api/v2/webchat/messages`;
// 1. Create the session via HTTP POST
const sessionPayload = {
webchatConfigId: WEB_CHAT_CONFIG_ID,
// Optional: Custom attributes for routing or identification
customAttributes: {
"userEmail": "guest@example.com",
"source": "custom-portal"
}
};
try {
const response = await fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(sessionPayload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Session initiation failed: ${response.status} - ${errorText}`);
}
const sessionData = await response.json();
// 2. Extract WebSocket connection details
const wsUrl = sessionData.connectionUrl;
const connectionToken = sessionData.connectionToken;
const guestId = sessionData.guestId;
console.log(`Session created. Guest ID: ${guestId}`);
// 3. Establish WebSocket Connection
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log("WebSocket connection established.");
// Send the connection token to authenticate the WebSocket stream
const authMessage = {
type: "auth",
token: connectionToken
};
ws.send(JSON.stringify(authMessage));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleIncomingMessage(message, ws);
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
};
ws.onclose = (event) => {
console.warn(`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`);
};
return { ws, guestId, sessionData };
} catch (error) {
console.error("Failed to initialize Web Messaging:", error);
throw error;
}
}
Expected Response from POST /api/v2/webchat/messages:
{
"id": "abc123-def456-ghi789",
"guestId": "guest-uuid-123",
"connectionUrl": "wss://12345678-1234-1234-1234-123456789012.mypurecloud.com/api/v2/webchat/ws",
"connectionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"webchatConfigId": "your-webchat-config-id",
"createdAt": "2023-10-27T10:00:00.000Z"
}
Step 2: Sending Typing Indicators
Typing indicators are sent as specific JSON payloads over the established WebSocket connection. The Genesys Cloud Web Messaging protocol expects a message with a specific type field.
Critical Parameter: The type field must be "typing" for the platform to recognize this as a status update rather than a chat message.
/**
* Sends a typing indicator to the agent.
* @param {WebSocket} ws - The active WebSocket connection.
*/
function sendTypingIndicator(ws) {
if (ws.readyState !== WebSocket.OPEN) {
console.warn("WebSocket is not open. Cannot send typing indicator.");
return;
}
const typingPayload = {
type: "typing",
// Optional: Client-side timestamp for consistency
timestamp: new Date().toISOString()
};
try {
ws.send(JSON.stringify(typingPayload));
console.log("Typing indicator sent.");
} catch (error) {
console.error("Failed to send typing indicator:", error);
}
}
// Example usage: Debounce user input to avoid flooding the agent
let typingTimeout = null;
function handleUserInput(e) {
// Clear previous timeout
if (typingTimeout) {
clearTimeout(typingTimeout);
}
// Send typing indicator immediately on first keystroke
sendTypingIndicator(ws);
// Reset timeout to stop sending after 2 seconds of inactivity
typingTimeout = setTimeout(() => {
// Optionally send a "stopped typing" signal if the protocol supports it,
// but Genesys typically treats absence of 'typing' events as stopped.
typingTimeout = null;
}, 2000);
}
Why this works: The Genesys Cloud platform listens for type: "typing" messages. Upon receipt, it updates the agent’s UI to show “Guest is typing…”. This does not create a permanent message in the transcript. It is ephemeral.
Step 3: Sending Read Receipts
Read receipts confirm that the guest has viewed a specific message. Unlike typing indicators, read receipts require a reference to the message ID.
Critical Parameter: The messageId field must match the id of the message sent by the agent.
/**
* Sends a read receipt for a specific message.
* @param {WebSocket} ws - The active WebSocket connection.
* @param {string} messageId - The ID of the message that was read.
*/
function sendReadReceipt(ws, messageId) {
if (ws.readyState !== WebSocket.OPEN) {
console.warn("WebSocket is not open. Cannot send read receipt.");
return;
}
const readReceiptPayload = {
type: "read",
messageId: messageId
};
try {
ws.send(JSON.stringify(readReceiptPayload));
console.log(`Read receipt sent for message: ${messageId}`);
} catch (error) {
console.error("Failed to send read receipt:", error);
}
}
Handling Incoming Messages: You must track incoming messages to know which ones to mark as read.
/**
* Handles incoming messages from the WebSocket.
* @param {Object} message - The parsed JSON message from the server.
* @param {WebSocket} ws - The active WebSocket connection.
*/
function handleIncomingMessage(message, ws) {
switch (message.type) {
case "message":
// Render the message in the UI
renderMessageInUI(message);
// If the message is visible in the viewport, send a read receipt
if (isMessageInViewport(message.id)) {
sendReadReceipt(ws, message.id);
}
break;
case "typing":
// Agent is typing
console.log("Agent is typing...");
updateAgentTypingUI(true);
break;
case "read":
// Agent has read a message
console.log(`Agent read message: ${message.messageId}`);
updateMessageReadStatus(message.messageId);
break;
default:
console.log("Unknown message type:", message.type);
}
}
// Helper function to simulate viewport check
function isMessageInViewport(messageId) {
// In a real app, this would check if the DOM element for the message is visible
// For this tutorial, we assume all messages are read upon receipt
return true;
}
Complete Working Example
This is a complete, copy-pasteable module that initializes the session, handles the WebSocket connection, and provides functions to send typing indicators and read receipts.
/**
* Genesys Cloud Web Messaging Client
* Handles session initiation, WebSocket management, and protocol-specific events.
*/
class GenesysWebChatClient {
constructor(orgId, webchatConfigId) {
this.orgId = orgId;
this.webchatConfigId = webchatConfigId;
this.ws = null;
this.guestId = null;
this.sessionData = null;
this.baseUrl = `https://${orgId}.mypurecloud.com/api/v2/webchat/messages`;
}
/**
* Starts the Web Messaging session.
*/
async start() {
try {
// 1. Initiate Session
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
webchatConfigId: this.webchatConfigId
})
});
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
this.sessionData = await response.json();
this.guestId = this.sessionData.guestId;
this.connectWebSocket();
} catch (error) {
console.error("Session start failed:", error);
throw error;
}
}
/**
* Establishes the WebSocket connection using the session token.
*/
connectWebSocket() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
return; // Already connected
}
this.ws = new WebSocket(this.sessionData.connectionUrl);
this.ws.onopen = () => {
console.log("WS Connected. Authenticating...");
this.ws.send(JSON.stringify({
type: "auth",
token: this.sessionData.connectionToken
}));
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (e) {
console.error("Failed to parse WS message:", e);
}
};
this.ws.onerror = (error) => {
console.error("WS Error:", error);
};
this.ws.onclose = () => {
console.warn("WS Closed. Attempting reconnect...");
// In production, implement exponential backoff reconnect logic here
};
}
/**
* Sends a typing indicator.
*/
sendTyping() {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: "typing",
timestamp: new Date().toISOString()
}));
}
}
/**
* Sends a read receipt for a specific message ID.
* @param {string} messageId - The ID of the message to mark as read.
*/
markAsRead(messageId) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: "read",
messageId: messageId
}));
}
}
/**
* Sends a text message.
* @param {string} text - The message content.
*/
sendMessage(text) {
if (this.ws.readyState === WebSocket.OPEN) {
// Generate a client-side ID for this message
const clientMessageId = crypto.randomUUID();
const messagePayload = {
type: "message",
id: clientMessageId,
text: text,
timestamp: new Date().toISOString()
};
this.ws.send(JSON.stringify(messagePayload));
// Stop typing indicator when message is sent
// (Optional: clear any pending typing timeouts)
}
}
/**
* Handles incoming messages from the server.
* @param {Object} message
*/
handleMessage(message) {
console.log("Received:", message);
if (message.type === "message") {
// Render message in UI
// Check if visible, then mark as read
this.markAsRead(message.id);
} else if (message.type === "typing") {
// Show agent typing indicator in UI
}
}
}
// Usage Example
// const client = new GenesysWebChatClient("your-org-id", "your-config-id");
// client.start().then(() => {
// client.sendTyping();
// setTimeout(() => {
// client.sendMessage("Hello, I need help.");
// }, 1000);
// });
Common Errors & Debugging
Error: WebSocket Connection Failed (401/403)
- Cause: The
connectionTokenis invalid, expired, or theconnectionUrlis incorrect. - Fix: Ensure you are using the
connectionUrlandconnectionTokenreturned from the initialPOST /api/v2/webchat/messagescall. Tokens are single-use for the initial handshake and expire quickly. Do not cache them across sessions.
Error: Typing Indicator Not Showing in Agent UI
- Cause: The
typefield is misspelled (e.g., “typingIndicator” instead of “typing”) or the payload is malformed. - Fix: Verify the JSON structure matches exactly:
{ "type": "typing" }. Genesys Cloud is strict about thetypefield value.
Error: Read Receipt Not Updating Agent UI
- Cause: The
messageIddoes not match theidof the message sent by the agent. - Fix: Ensure you are extracting the
idfield from the incoming message payload (message.id) and passing it tosendReadReceipt. Do not generate your own ID for read receipts; you are acknowledging an existing message.
Error: WebSocket 429 Too Many Requests
- Cause: Sending typing indicators too frequently.
- Fix: Implement debouncing. Do not send a typing indicator on every keystroke. Send one when the user starts typing, and optionally another if they continue typing after a pause. A 1-2 second debounce interval is recommended.