How to Control Agent Microphone State via Genesys Cloud Web Chat and WebSocket APIs
What You Will Build
- A functional JavaScript module that sends WebSocket commands to the Genesys Cloud Edge to mute and unmute an agent’s microphone during an active conversation.
- This tutorial utilizes the Genesys Cloud WebSocket Client API, specifically the
sendmethod on the active connection, rather than REST endpoints. - The implementation covers JavaScript/TypeScript using the official
@genesyscloud/purecloud-platform-client-v2SDK for authentication and raw WebSocket handling for real-time control.
Prerequisites
- OAuth Client Type: Public or Confidential client with the
webchat:clientscope. Note that traditional REST scopes likeconversation:vieware insufficient for WebSocket client-side actions. - SDK Version:
@genesyscloud/purecloud-platform-client-v2v4.0.0 or higher. - Runtime: Node.js 18+ or a modern browser environment (Chrome, Firefox, Edge).
- Dependencies:
@genesyscloud/purecloud-platform-client-v2dotenv(for local environment variable management)
Authentication Setup
The Genesys Cloud WebSocket client does not authenticate via HTTP headers in the same way REST APIs do. Instead, it authenticates via an initial auth message sent over the WebSocket connection. This message requires a valid OAuth access token.
First, you must generate an OAuth token. For this tutorial, we assume a Confidential Client flow (Client Credentials) for service-to-service testing, or a Public Client flow (Authorization Code) for end-user agent applications.
// auth-helper.js
const { PlatformClient } = require('@genesyscloud/purecloud-platform-client-v2');
/**
* Generates an OAuth Access Token using Client Credentials Flow.
* Replace these values with your actual Genesys Cloud API credentials.
*/
async function getAccessToken() {
const platformClient = PlatformClient.create();
const config = {
clientId: process.env.GENESYS_CLIENT_ID,
clientSecret: process.env.GENESYS_CLIENT_SECRET,
environment: process.env.GENESYS_ENVIRONMENT || 'mypurecloud.com' // e.g., euw2.pure.cloud
};
try {
const response = await platformClient.login(config);
return response.body.access_token;
} catch (error) {
console.error('Failed to authenticate:', error.message);
throw error;
}
}
module.exports = { getAccessToken };
Critical Note: The token must have the webchat:client scope. If you are using a client registered for standard REST API access, you may need to add this scope in the Developer Console under your API Client settings. Without this scope, the WebSocket handshake will succeed, but the server will reject your auth message with an unauthorized error.
Implementation
Step 1: Establish the WebSocket Connection
The Genesys Cloud Edge provides a WebSocket endpoint. The URL structure is wss://webchat-edge.genesiscloud.com/ws?environment={env}. You must construct the URL correctly based on your environment.
We will create a wrapper class that manages the connection lifecycle and handles the initial authentication handshake.
// websocket-client.js
const { getAccessToken } = require('./auth-helper');
class GenesysWebSocketClient {
constructor(environment) {
this.environment = environment;
this.ws = null;
this.isConnected = false;
this.isAuthorized = false;
// Construct the WebSocket URL
// For US: wss://webchat-edge.genesiscloud.com/ws?environment=us-east-1
// For EU: wss://webchat-edge.euw2.genesiscloud.com/ws?environment=euw2
const edgeHost = environment === 'euw2'
? 'webchat-edge.euw2.genesiscloud.com'
: 'webchat-edge.genesiscloud.com';
this.wsUrl = `wss://${edgeHost}/ws?environment=${environment}`;
}
async connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.wsUrl);
this.ws.onopen = () => {
console.log('WebSocket connection established.');
this.isConnected = true;
this.authenticate().then(resolve).catch(reject);
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
this.ws.onclose = (event) => {
console.log(`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`);
this.isConnected = false;
this.isAuthorized = false;
};
});
}
async authenticate() {
try {
const token = await getAccessToken();
const authMessage = {
type: 'auth',
payload: {
token: token
}
};
this.send(JSON.stringify(authMessage));
// Wait for the 'authed' response from the server
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Authentication timed out.'));
}, 5000);
const originalHandler = this.ws.onmessage;
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'authed') {
clearTimeout(timeout);
this.isAuthorized = true;
console.log('Successfully authenticated with Genesys Cloud.');
this.ws.onmessage = originalHandler; // Restore handler
resolve();
} else if (data.type === 'error') {
clearTimeout(timeout);
this.ws.onmessage = originalHandler;
reject(new Error(`Auth failed: ${data.payload.message}`));
}
};
});
} catch (error) {
throw new Error('Failed to retrieve or send auth token: ' + error.message);
}
}
send(message) {
if (!this.isConnected || this.ws.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket is not connected.');
}
this.ws.send(message);
}
handleMessage(data) {
try {
const message = JSON.parse(data);
console.log('Received:', JSON.stringify(message, null, 2));
// Handle specific response types if needed
if (message.type === 'error') {
console.error('Server Error:', message.payload);
}
} catch (e) {
console.error('Failed to parse message:', data);
}
}
disconnect() {
if (this.ws) {
this.ws.close();
}
}
}
module.exports = { GenesysWebSocketClient };
Step 2: Implement Mute/Unmute Logic
Once authenticated, you can send control messages. To mute or unmute an agent, you send a media event with the mute action.
The payload structure depends on whether you are controlling audio or video. For microphone control, we target the audio stream.
Key Payload Fields:
type: Must be'media'.action: Must be'mute'or'unmute'.streamId: Optional but recommended. If omitted, it applies to the default stream. For precise control in multi-stream scenarios, identify the stream ID from themediaevents received during call setup.direction:'outbound'(what the agent sends) or'inbound'(what the agent hears). To mute the microphone, you are muting theoutboundaudio.
// media-controller.js
const { GenesysWebSocketClient } = require('./websocket-client');
class MediaController {
constructor(environment) {
this.client = new GenesysWebSocketClient(environment);
}
async initialize() {
await this.client.connect();
}
/**
* Mutes the agent's microphone (outbound audio).
* @param {string} [conversationId] - Optional conversation ID for logging/tracking.
*/
async muteMicrophone(conversationId = 'unknown') {
if (!this.client.isAuthorized) {
throw new Error('Client is not authenticated.');
}
const payload = {
type: 'media',
action: 'mute',
streamId: 'default', // Use specific streamId if known
direction: 'outbound', // Muting what we send
mediaType: 'audio'
};
console.log(`[Mute] Sending mute command for conversation: ${conversationId}`);
this.client.send(JSON.stringify(payload));
// Note: The server may acknowledge this with a 'media' event of type 'mute'
// or it may be fire-and-forget depending on the edge configuration.
}
/**
* Unmutes the agent's microphone (restores outbound audio).
* @param {string} [conversationId] - Optional conversation ID for logging/tracking.
*/
async unmuteMicrophone(conversationId = 'unknown') {
if (!this.client.isAuthorized) {
throw new Error('Client is not authenticated.');
}
const payload = {
type: 'media',
action: 'unmute',
streamId: 'default',
direction: 'outbound',
mediaType: 'audio'
};
console.log(`[Unmute] Sending unmute command for conversation: ${conversationId}`);
this.client.send(JSON.stringify(payload));
}
/**
* Toggles the microphone state.
* Requires tracking current state externally.
*/
async toggleMicrophone(isMuted, conversationId = 'unknown') {
if (isMuted) {
await this.muteMicrophone(conversationId);
} else {
await this.unmuteMicrophone(conversationId);
}
}
disconnect() {
this.client.disconnect();
}
}
module.exports = { MediaController };
Step 3: Processing Results and State Sync
The Genesys Cloud WebSocket protocol is event-driven. When you send a mute command, the server might respond with a confirmation event. More importantly, if the mute state changes on the server side (e.g., by a supervisor or another client), you will receive a media event.
You must handle these incoming events to keep your UI in sync.
// event-handler.js
/**
* Attaches a listener for media state changes to update local UI state.
*/
function attachMediaStateListener(webSocketClient, onStateChange) {
const originalHandler = webSocketClient.client.ws.onmessage;
webSocketClient.client.ws.onmessage = (event) => {
// Call original handler to ensure base logging happens
if (originalHandler) originalHandler(event);
try {
const data = JSON.parse(event.data);
// Listen for media events that indicate state changes
if (data.type === 'media') {
const payload = data.payload;
if (payload.action === 'mute' && payload.direction === 'outbound' && payload.mediaType === 'audio') {
console.log('State Sync: Microphone is now MUTED.');
onStateChange(true, payload.streamId);
}
else if (payload.action === 'unmute' && payload.direction === 'outbound' && payload.mediaType === 'audio') {
console.log('State Sync: Microphone is now UNMUTED.');
onStateChange(false, payload.streamId);
}
}
// Handle errors specifically related to media actions
if (data.type === 'error' && data.payload && data.payload.action === 'mute') {
console.error('Failed to mute/unmute:', data.payload.message);
}
} catch (e) {
console.error('Error parsing media event:', e);
}
};
}
module.exports = { attachMediaStateListener };
Complete Working Example
Below is a complete, runnable Node.js script that initializes the connection, mutes the microphone, waits 5 seconds, unmutes it, and then disconnects.
// main.js
require('dotenv').config();
const { MediaController } = require('./media-controller');
const { attachMediaStateListener } = require('./event-handler');
async function main() {
const ENVIRONMENT = process.env.GENESYS_ENVIRONMENT || 'us-east-1';
const CONVERSATION_ID = 'test-conversation-123'; // Placeholder for demo
const controller = new MediaController(ENVIRONMENT);
try {
// 1. Initialize and Authenticate
console.log('Connecting to Genesys Cloud Edge...');
await controller.initialize();
console.log('Connection and Authentication successful.');
// 2. Attach State Listener
attachMediaStateListener(controller, (isMuted, streamId) => {
console.log(`UI Update: Microphone state changed to ${isMuted ? 'MUTED' : 'UNMUTED'} on stream ${streamId}`);
});
// 3. Mute Microphone
await controller.muteMicrophone(CONVERSATION_ID);
// Wait to observe the mute state
await new Promise(resolve => setTimeout(resolve, 5000));
// 4. Unmute Microphone
await controller.unmuteMicrophone(CONVERSATION_ID);
// Wait to observe the unmute state
await new Promise(resolve => setTimeout(resolve, 5000));
} catch (error) {
console.error('Execution failed:', error.message);
process.exit(1);
} finally {
// 5. Cleanup
controller.disconnect();
console.log('Disconnected.');
process.exit(0);
}
}
main();
Common Errors & Debugging
Error: WebSocket Connection Refused (403 or 503)
- What causes it: The environment URL is incorrect, or the client IP is blocked by firewall rules.
- How to fix it: Verify the
GENESYS_ENVIRONMENTvariable. Ensure you are using the correct edge host (webchat-edge.genesiscloud.comfor US,webchat-edge.euw2.genesiscloud.comfor EU). Check that your network allows outbound traffic on port 443 to these domains.
Error: Auth Failed: Invalid Token
- What causes it: The OAuth token provided in the
authmessage is expired, invalid, or lacks thewebchat:clientscope. - How to fix it:
- Check your API Client settings in the Genesys Cloud Admin Console. Ensure the
webchat:clientscope is added. - Verify that the
getAccessTokenfunction is retrieving a fresh token. Tokens expire after 1 hour by default. - Log the raw token being sent to ensure it is not empty or malformed.
- Check your API Client settings in the Genesys Cloud Admin Console. Ensure the
Error: Media Action Failed: Stream Not Found
- What causes it: You specified a
streamIdthat does not exist in the current session. - How to fix it: If you do not know the specific
streamId, omit thestreamIdfield from the payload. The server will apply the action to the default active stream. Alternatively, inspect themediaevents received at the start of the call to identify active stream IDs.
Error: 429 Too Many Requests
- What causes it: You are sending media control messages too frequently. Genesys Cloud enforces rate limits on WebSocket messages.
- How to fix it: Implement a debounce mechanism in your UI layer. Do not send a mute/unmute command if the current state matches the desired state. Cache the last known state locally and compare before sending.
// Debounce example
let lastMuteState = null;
function requestMute(isMuted) {
if (lastMuteState === isMuted) {
console.log('State unchanged, skipping send.');
return;
}
lastMuteState = isMuted;
controller.toggleMicrophone(isMuted);
}