Customizing Genesys Cloud Web Messaging Rich Message Rendering by Extending the Client SDK Component Tree and Injecting Custom React Hooks for Interactive Card Actions
What You Will Build
- One sentence: This tutorial demonstrates how to intercept Genesys Cloud interactive card messages, render them with a custom React component, and execute backend API calls with retry logic and state management through a custom hook.
- One sentence: This implementation uses the
@genesyscloud/web-messaging-client-sdkReact component tree and the Genesys Cloud REST API for interaction event tracking. - One sentence: The code is written in TypeScript with React and utilizes modern async/await patterns with the native fetch API.
Prerequisites
- OAuth client type and required scopes: The Web Messaging Client SDK uses deployment-based authentication. If your custom hook calls Genesys Cloud REST APIs directly, you require a confidential OAuth client with the
interaction:event:sendscope. Backend proxy endpoints must handle token exchange securely. - SDK version or API version:
@genesyscloud/web-messaging-client-sdkv2.4.0 or later. REST API v2. - Language/runtime requirements: Node.js 18+, React 18+, TypeScript 5.0+
- Any external dependencies:
@genesyscloud/web-messaging-client-sdk,react,react-dom,typescript
Authentication Setup
The Web Messaging Client SDK does not require manual OAuth token management for standard chat sessions. Authentication is handled through the deployment configuration object. You must provide a valid deploymentId and apiServer endpoint. If your custom actions call Genesys Cloud APIs directly, you must implement a secure backend token exchange. The following configuration object initializes the SDK with regional routing and deployment binding.
import { configuration } from '@genesyscloud/web-messaging-client-sdk';
export const webMessagingConfig = configuration.create({
apiServer: 'https://api.mypurecloud.com',
deploymentId: 'your-deployment-id-here',
region: 'us-east-1',
logger: {
logLevel: 'warn',
console: console
}
});
Token caching is managed internally by the SDK. If you extend the client to call REST endpoints, implement a token cache with TTL validation. The following example shows a minimal token cache pattern for backend proxy calls.
interface TokenCache {
accessToken: string;
expiresAt: number;
}
const tokenCache: TokenCache = {
accessToken: '',
expiresAt: 0
};
export async function getGenesysAccessToken(): Promise<string> {
if (Date.now() < tokenCache.expiresAt) {
return tokenCache.accessToken;
}
const response = await fetch('https://api.mypurecloud.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.GENESYS_CLIENT_ID || '',
client_secret: process.env.GENESYS_CLIENT_SECRET || '',
scope: 'interaction:event:send'
})
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`OAuth token request failed with ${response.status}: ${errorBody}`);
}
const data = await response.json();
tokenCache.accessToken = data.access_token;
tokenCache.expiresAt = Date.now() + (data.expires_in * 1000) - 30000;
return data.access_token;
}
Implementation
Step 1: Initialize the Custom Interactive Card Hook
The custom hook manages the lifecycle of card button interactions. It handles API calls, loading states, error boundaries, and retry logic for rate limits. The hook accepts a callback that executes when the user interacts with a card button.
import { useState, useCallback } from 'react';
interface CardActionState {
loading: boolean;
error: string | null;
success: boolean;
}
interface CardActionPayload {
interactionId: string;
buttonLabel: string;
customData?: Record<string, unknown>;
}
export function useInteractiveCardAction(apiEndpoint: string) {
const [state, setState] = useState<CardActionState>({
loading: false,
error: null,
success: false
});
const executeAction = useCallback(async (payload: CardActionPayload) => {
setState({ loading: true, error: null, success: false });
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
const token = await getGenesysAccessToken();
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
type: 'webchat',
interactionId: payload.interactionId,
event: {
type: 'button-click',
payload: {
label: payload.buttonLabel,
metadata: payload.customData
}
}
})
});
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.pow(2, attempt) * 1000;
attempt++;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (response.status === 401 || response.status === 403) {
throw new Error(`Authentication failed: ${response.status}. Verify OAuth scope interaction:event:send.`);
}
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed with ${response.status}: ${errorText}`);
}
setState({ loading: false, error: null, success: true });
return;
} catch (err) {
if (attempt === maxRetries - 1) {
setState({ loading: false, error: err instanceof Error ? err.message : 'Unknown error occurred', success: false });
}
attempt++;
}
}
}, [apiEndpoint]);
return { ...state, executeAction };
}
Step 2: Build the Custom Card Component
The component overrides the default interactiveMessage renderer. It receives standard SDK props and maps them to custom UI logic. The component extracts button data from the Genesys Cloud rich message structure and binds click handlers to the custom hook.
import React, { memo } from 'react';
import { MessageComponentProps } from '@genesyscloud/web-messaging-client-sdk';
import { useInteractiveCardAction } from './useInteractiveCardAction';
interface InteractiveCardButton {
label: string;
value: string;
type: string;
}
interface RichMessagePayload {
buttons: InteractiveCardButton[];
title?: string;
description?: string;
image?: string;
}
const CustomInteractiveCard: React.FC<MessageComponentProps> = memo(({
message,
onSendMessage,
onSelectOption
}) => {
const payload = message.payload as RichMessagePayload;
const { loading, error, success, executeAction } = useInteractiveCardAction(
'https://api.mypurecloud.com/api/v2/interactions/events/post:webchat'
);
const handleButtonClick = async (button: InteractiveCardButton) => {
await executeAction({
interactionId: message.interactionId,
buttonLabel: button.label,
customData: { messageId: message.id, timestamp: Date.now() }
});
if (!error) {
onSendMessage({
type: 'text',
text: button.value
});
onSelectOption?.(button.value);
}
};
return (
<div style={{ border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', marginTop: '8px' }}>
{payload.title && <h3 style={{ margin: '0 0 8px 0' }}>{payload.title}</h3>}
{payload.description && <p style={{ margin: '0 0 12px 0', color: '#666' }}>{payload.description}</p>}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{payload.buttons.map((button, index) => (
<button
key={index}
disabled={loading || success}
onClick={() => handleButtonClick(button)}
style={{
padding: '10px 16px',
background: loading ? '#ccc' : '#0066cc',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: loading ? 'not-allowed' : 'pointer'
}}
>
{loading ? 'Processing...' : button.label}
</button>
))}
</div>
{error && (
<div style={{ marginTop: '8px', color: '#d32f2f', fontSize: '12px' }}>
Action failed: {error}
</div>
)}
{success && !error && (
<div style={{ marginTop: '8px', color: '#2e7d32', fontSize: '12px' }}>
Action recorded successfully.
</div>
)}
</div>
);
});
CustomInteractiveCard.displayName = 'CustomInteractiveCard';
export default CustomInteractiveCard;
Step 3: Inject the Component into the SDK Tree
The final step registers the custom component with the messageComponents prop. The SDK replaces the default renderer for interactiveMessage types with your component. All other message types continue using the default renderers.
import React from 'react';
import { WebMessaging } from '@genesyscloud/web-messaging-client-sdk';
import { webMessagingConfig } from './config';
import CustomInteractiveCard from './CustomInteractiveCard';
export const WebMessagingWidget: React.FC = () => {
return (
<WebMessaging
configuration={webMessagingConfig}
messageComponents={{
interactiveMessage: CustomInteractiveCard
}}
onMessageReceived={(message) => {
console.log('Message received:', message.type, message.id);
}}
onError={(error) => {
console.error('Web Messaging SDK error:', error);
}}
/>
);
};
Complete Working Example
The following module combines configuration, hook logic, component definition, and SDK injection into a single deployable file. Replace placeholder credentials before execution.
import React, { useCallback, useState, memo } from 'react';
import { WebMessaging, configuration, MessageComponentProps } from '@genesyscloud/web-messaging-client-sdk';
// 1. Configuration
const webMessagingConfig = configuration.create({
apiServer: 'https://api.mypurecloud.com',
deploymentId: 'YOUR_DEPLOYMENT_ID',
region: 'us-east-1',
logger: { logLevel: 'warn', console: console }
});
// 2. Token Cache & OAuth Helper
const tokenCache = { accessToken: '', expiresAt: 0 };
async function getGenesysAccessToken(): Promise<string> {
if (Date.now() < tokenCache.expiresAt) return tokenCache.accessToken;
const response = await fetch('https://api.mypurecloud.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
scope: 'interaction:event:send'
})
});
if (!response.ok) throw new Error(`OAuth failed: ${response.status}`);
const data = await response.json();
tokenCache.accessToken = data.access_token;
tokenCache.expiresAt = Date.now() + (data.expires_in * 1000) - 30000;
return data.access_token;
}
// 3. Custom Hook
function useInteractiveCardAction(apiEndpoint: string) {
const [state, setState] = useState({ loading: false, error: null as string | null, success: false });
const executeAction = useCallback(async (payload: { interactionId: string; buttonLabel: string }) => {
setState({ loading: true, error: null, success: false });
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
const token = await getGenesysAccessToken();
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'webchat',
interactionId: payload.interactionId,
event: { type: 'button-click', payload: { label: payload.buttonLabel } }
})
});
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
await new Promise(res => setTimeout(res, retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.pow(2, attempt) * 1000));
attempt++;
continue;
}
if (response.status === 401 || response.status === 403) {
throw new Error(`Auth error: ${response.status}`);
}
if (!response.ok) throw new Error(`API error: ${response.status}`);
setState({ loading: false, error: null, success: true });
return;
} catch (err) {
if (attempt === maxRetries - 1) {
setState({ loading: false, error: err instanceof Error ? err.message : 'Request failed', success: false });
}
attempt++;
}
}
}, [apiEndpoint]);
return { ...state, executeAction };
}
// 4. Custom Component
const CustomInteractiveCard: React.FC<MessageComponentProps> = memo(({ message, onSendMessage }) => {
const payload = (message.payload as any) || { buttons: [] };
const { loading, error, success, executeAction } = useInteractiveCardAction(
'https://api.mypurecloud.com/api/v2/interactions/events/post:webchat'
);
const handleBtn = async (btn: { label: string; value: string }) => {
await executeAction({ interactionId: message.interactionId, buttonLabel: btn.label });
if (!error) onSendMessage({ type: 'text', text: btn.value });
};
return (
<div style={{ border: '1px solid #ccc', padding: 12, borderRadius: 6 }}>
{payload.buttons.map((b: { label: string; value: string }, i: number) => (
<button key={i} disabled={loading || success} onClick={() => handleBtn(b)} style={{ marginRight: 8 }}>
{loading ? 'Loading...' : b.label}
</button>
))}
{error && <div style={{ color: 'red', fontSize: 12 }}>{error}</div>}
{success && !error && <div style={{ color: 'green', fontSize: 12 }}>Success</div>}
</div>
);
});
// 5. Main Widget
export const App: React.FC = () => (
<WebMessaging
configuration={webMessagingConfig}
messageComponents={{ interactiveMessage: CustomInteractiveCard }}
onError={(e) => console.error('SDK Error:', e)}
/>
);
Common Errors & Debugging
Error: 401 Unauthorized or 403 Forbidden on Interaction Events
- What causes it: The OAuth client lacks the
interaction:event:sendscope, or the token has expired. The deployment configuration may also point to an incorrect region. - How to fix it: Verify the client credentials in Genesys Cloud Admin under Security > OAuth Clients. Ensure the scope matches exactly. Validate that the
apiServerregion matches your deployment region. - Code showing the fix:
// Verify scope matches exactly
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.GENESYS_CLIENT_ID || '',
client_secret: process.env.GENESYS_CLIENT_SECRET || '',
scope: 'interaction:event:send' // Exact match required
})
Error: 429 Too Many Requests Cascading Across Microservices
- What causes it: Rapid button clicks or automated testing triggers rate limits on the interaction events endpoint. Genesys Cloud enforces per-client and per-endpoint limits.
- How to fix it: Implement exponential backoff with jitter and respect the
Retry-Afterheader. Disable buttons during loading states to prevent duplicate submissions. - Code showing the fix:
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const jitter = Math.random() * 1000;
const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.pow(2, attempt) * 1000 + jitter;
await new Promise(resolve => setTimeout(resolve, delay));
attempt++;
continue;
}
Error: MessageComponentProps Type Mismatch or Undefined Payload
- What causes it: The custom component expects a specific rich message structure, but the agent sent a malformed card or a different message type. The SDK passes raw payload objects without strict schema validation.
- How to fix it: Add defensive type checking and fallback rendering. Validate the presence of
buttonsarray before mapping. - Code showing the fix:
const payload = message.payload as { buttons?: Array<{ label: string; value: string }> } | undefined;
if (!payload?.buttons || !Array.isArray(payload.buttons)) {
return <div style={{ color: '#999' }}>Unsupported card format</div>;
}