Sending Typing Indicators and Read Receipts via the Genesys Cloud Web Messaging Guest API

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 fetch API and native WebSocket.
  • Dependencies: None. This uses standard browser APIs. No external npm packages are required for the core logic, though uuid or 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.

  1. Initiate Session: You must first call the POST /api/v2/webchat/messages endpoint to create a new Web Messaging session.
  2. Receive Connection Info: The response contains a connectionUrl (WebSocket endpoint) and a connectionToken.
  3. Establish WebSocket: Connect to the connectionUrl using the connectionToken in 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 connectionToken is invalid, expired, or the connectionUrl is incorrect.
  • Fix: Ensure you are using the connectionUrl and connectionToken returned from the initial POST /api/v2/webchat/messages call. 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 type field 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 the type field value.

Error: Read Receipt Not Updating Agent UI

  • Cause: The messageId does not match the id of the message sent by the agent.
  • Fix: Ensure you are extracting the id field from the incoming message payload (message.id) and passing it to sendReadReceipt. 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.

Official References