Managing Genesys Cloud Wrap-Up Code Configurations via API with Node.js
What You Will Build
- A Node.js module that creates, validates, and synchronizes Genesys Cloud wrap-up codes with routing queues, external CRM disposition fields, and analytics pipelines.
- This uses the Genesys Cloud REST API and the
@genesyscloud/sdk-corefeature packages for routing, analytics, and platform logging. - The implementation covers ES modules,
async/await, ETag conflict resolution, 429 retry logic, and deterministic workflow simulation.
Prerequisites
- OAuth 2.0 Client Credentials flow configured in Genesys Cloud
- Required scopes:
wrapupcodes:read,wrapupcodes:write,routing:queue:read,routing:queue:write,analytics:conversations:read,platform:log:read - Node.js 18+ with
@genesyscloud/sdk-core,@genesyscloud/sdk-features-routing,@genesyscloud/sdk-features-analytics,@genesyscloud/sdk-features-platform - Environment variables:
GENESYS_REGION,GENESYS_CLIENT_ID,GENESYS_CLIENT_SECRET,GENESYS_ORG_ID
Authentication Setup
The client credentials flow issues a bearer token that expires after one hour. The code below caches the token and handles refresh automatically.
import https from 'https';
import { URL } from 'url';
const API_HOST = `${process.env.GENESYS_REGION}.mypurecloud.com`;
/**
* Retrieves or refreshes an OAuth 2.0 bearer token.
* @returns {Promise<string>} Bearer token string
*/
export async function getAuthToken() {
const tokenUrl = `https://${API_HOST}/login/oauth2/token`;
const payload = new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.GENESYS_CLIENT_ID,
client_secret: process.env.GENESYS_CLIENT_SECRET
});
const response = await fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: payload
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`OAuth token request failed with status ${response.status}: ${errorText}`);
}
const data = await response.json();
return data.access_token;
}
Implementation
Step 1: Constructing and Creating Wrap-Up Definitions
Wrap-up codes require a unique name, description, category, and optional wrapupCode identifier. The duration field enforces post-interaction handling time. Mandatory flags are controlled via isMandatory.
Required Scope: wrapupcodes:write
import { genesysCloudSdkCore } from '@genesyscloud/sdk-core';
import { features } from '@genesyscloud/sdk-features-routing';
const client = new genesysCloudSdkCore.V2Client();
export async function createWrapUpCode(config) {
const token = await getAuthToken();
client.setAuthClient({
getAccessToken: async () => token,
refreshAccessToken: async () => { throw new Error('Static token flow does not support refresh'); }
});
const body = {
name: config.name,
description: config.description,
wrapupCode: config.wrapupCode,
category: config.category,
duration: config.duration || 0,
isMandatory: config.isMandatory || false,
isDefault: config.isDefault || false,
routingQueueIds: config.queueIds || []
};
try {
const result = await client.platformClient.wrapupCodes.postWrapupcodes(body);
return result.body;
} catch (err) {
if (err.status === 409) throw new Error('Wrap-up code already exists with this wrapupCode identifier.');
if (err.status === 429) throw new Error('Rate limit exceeded. Implement backoff.');
throw err;
}
}
Step 2: Validating Against Interaction Types and Queue Mappings
Wrap-up codes must align with queue interaction types (voice, chat, social, email) and agent skill requirements. The validation function fetches queue configuration and verifies compatibility.
Required Scope: routing:queue:read
export async function validateWrapUpAgainstQueue(queueId, wrapUpCodeId) {
const token = await getAuthToken();
client.setAuthClient({ getAccessToken: async () => token, refreshAccessToken: async () => { throw new Error('No refresh'); } });
const queueRes = await client.platformClient.routing.getRoutingqueuesqueueId(queueId);
const queue = queueRes.body;
const requiredSkills = queue.skillRequirements || [];
const interactionTypes = queue.interactionTypes || [];
if (!interactionTypes.length) {
throw new Error(`Queue ${queueId} has no interaction types configured. Wrap-up codes cannot be assigned.`);
}
const compatibilityMap = {
voice: ['phone', 'callback'],
chat: ['webchat', 'mobilechat'],
social: ['facebook', 'twitter'],
email: ['email']
};
const validChannels = interactionTypes.flatMap(type => compatibilityMap[type] || []);
return {
queueId,
wrapUpCodeId,
supportedChannels: validChannels,
requiredSkills,
isValid: validChannels.length > 0
};
}
Step 3: Handling Asynchronous Activation and Version Control
Configuration propagation to edge nodes requires polling. ETags (If-Match) prevent race conditions during concurrent updates. The retry wrapper handles 429 responses with exponential backoff.
async function retryOn429(fn, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (err.status === 429 && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw err;
}
}
}
export async function pollQueueActivation(queueId, expectedWrapUpCodeId, timeoutMs = 30000) {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
await retryOn429(async () => {
const res = await client.platformClient.routing.getRoutingqueuesqueueId(queueId);
const currentEtag = res.headers.etag;
const queue = res.body;
const assignedCodes = queue.wrapupCodes || [];
const isAssigned = assignedCodes.some(c => c.id === expectedWrapUpCodeId);
if (isAssigned) {
return { status: 'active', etag: currentEtag };
}
throw new Error('Not yet propagated');
});
await new Promise(resolve => setTimeout(resolve, 2000));
}
throw new Error('Activation timeout exceeded');
}
Step 4: Synchronizing with External CRM Disposition Fields
Genesys custom attributes map wrap-up codes to CRM disposition fields. The batch update function pushes mapped values to an external endpoint using attribute mapping rules.
Required Scope: attributes:read (or outbound:contactlists:write for batch)
export async function syncWrapUpToCrm(wrapUpCodeId, dispositionMapping, crmEndpoint) {
const token = await getAuthToken();
client.setAuthClient({ getAccessToken: async () => token, refreshAccessToken: async () => { throw new Error('No refresh'); } });
const wrapUpRes = await client.platformClient.wrapupCodes.getWrapupcodeswrapupCodeId(wrapUpCodeId);
const wrapUp = wrapUpRes.body;
const payload = {
wrapUpCode: wrapUp.wrapupCode,
category: wrapUp.category,
crmDisposition: dispositionMapping[wrapUp.wrapupCode] || 'unknown',
syncTimestamp: new Date().toISOString()
};
const response = await fetch(crmEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`CRM sync failed: ${response.status} ${errorText}`);
}
return response.json();
}
Step 5: Tracking Usage Metrics and Assignment Accuracy
The analytics summary query aggregates wrap-up usage by conversation count and average duration. Grouping by wrapupCode enables QA accuracy tracking.
Required Scope: analytics:conversations:read
export async function getWrapUpMetrics(startDate, endDate) {
const token = await getAuthToken();
client.setAuthClient({ getAccessToken: async () => token, refreshAccessToken: async () => { throw new Error('No refresh'); } });
const body = {
dateFrom: startDate,
dateTo: endDate,
groupBys: ['wrapupCode'],
metrics: ['conversation/count', 'conversation/avgDuration'],
queryType: 'conversation'
};
try {
const res = await client.platformClient.analytics.postAnalyticsconversationssummaryquery(body);
return res.body.entities || [];
} catch (err) {
if (err.status === 429) throw new Error('Analytics rate limit hit. Reduce query frequency.');
throw err;
}
}
Step 6: Generating Compliance Audit Logs
The platform log query API captures configuration changes. Filtering by wrapupcodes and routing/queues provides a compliance trail. Pagination handles large result sets.
Required Scope: platform:log:read
export async function getWrapUpAuditLogs(startDate, endDate, pageSize = 25) {
const token = await getAuthToken();
client.setAuthClient({ getAccessToken: async () => token, refreshAccessToken: async () => { throw new Error('No refresh'); } });
const body = {
dateFrom: startDate,
dateTo: endDate,
pageSize: pageSize,
query: 'type:wrapupcodes OR type:routing/queues'
};
const logs = [];
let paginationCursor = null;
do {
const res = await client.platformClient.platform.postPlatformlogquery(body);
logs.push(...res.body.entities);
paginationCursor = res.body.paginationView?.nextPageCursor;
body.paginationCursor = paginationCursor;
} while (paginationCursor);
return logs;
}
Step 7: Exposing a Wrap-Up Simulator for Workflow Testing
The simulator validates wrap-up assignment logic before production deployment. It checks mandatory flags, duration constraints, and skill alignment against mock agent profiles.
export class WrapUpSimulator {
constructor(agentSkills, queueInteractionTypes) {
this.agentSkills = agentSkills;
this.interactionTypes = queueInteractionTypes;
}
simulateAssignment(wrapUpCodeConfig) {
const errors = [];
if (wrapUpCodeConfig.isMandatory && !wrapUpCodeConfig.wrapupCode) {
errors.push('Mandatory wrap-up code missing identifier.');
}
if (wrapUpCodeConfig.duration > 0 && wrapUpCodeConfig.duration > 300) {
errors.push('Duration exceeds maximum allowed post-interaction time of 300 seconds.');
}
const skillMatch = this.agentSkills.every(skill =>
wrapUpCodeConfig.requiredSkills?.includes(skill)
);
if (!skillMatch) {
errors.push('Agent skill requirements do not match wrap-up code configuration.');
}
return {
isValid: errors.length === 0,
errors,
simulatedOutcome: errors.length === 0 ? 'approved' : 'rejected'
};
}
}
Complete Working Example
The following script orchestrates creation, validation, activation polling, CRM sync, metrics retrieval, audit logging, and simulation. Replace credential placeholders before execution.
import { createWrapUpCode, validateWrapUpAgainstQueue, pollQueueActivation, syncWrapUpToCrm, getWrapUpMetrics, getWrapUpAuditLogs, WrapUpSimulator, getAuthToken } from './wrapup-manager.js';
async function main() {
try {
const wrapUpConfig = {
name: 'Sale Completed - Enterprise',
description: 'Wrap-up for closed enterprise deals exceeding $50k',
wrapupCode: 'SALE_ENT_50K',
category: 'sales',
duration: 45,
isMandatory: true,
queueIds: ['queue-uuid-1234567890']
};
console.log('Creating wrap-up code...');
const created = await createWrapUpCode(wrapUpConfig);
console.log('Created:', created.id);
console.log('Validating against queue...');
const validation = await validateWrapUpAgainstQueue(wrapUpConfig.queueIds[0], created.id);
if (!validation.isValid) throw new Error('Validation failed.');
console.log('Polling activation...');
const activation = await pollQueueActivation(wrapUpConfig.queueIds[0], created.id);
console.log('Activation status:', activation.status);
console.log('Syncing to CRM...');
const crmResult = await syncWrapUpToCrm(
created.id,
{ 'SALE_ENT_50K': 'WON_ENTERPRISE', 'SALE_SMB_10K': 'WON_SMB' },
'https://crm.example.com/api/v1/dispositions/sync'
);
console.log('CRM Sync:', crmResult);
console.log('Simulating assignment...');
const simulator = new WrapUpSimulator(['sales_enterprise', 'negotiation'], ['voice', 'email']);
const simResult = simulator.simulateAssignment({
wrapupCode: 'SALE_ENT_50K',
isMandatory: true,
duration: 45,
requiredSkills: ['sales_enterprise', 'negotiation']
});
console.log('Simulation:', simResult);
console.log('Fetching metrics and audit logs...');
const metrics = await getWrapUpMetrics('2024-01-01T00:00:00.000Z', '2024-12-31T23:59:59.999Z');
const logs = await getWrapUpAuditLogs('2024-01-01T00:00:00.000Z', '2024-12-31T23:59:59.999Z');
console.log('Metrics count:', metrics.length);
console.log('Audit log count:', logs.length);
} catch (err) {
console.error('Workflow failed:', err.message);
process.exit(1);
}
}
main();
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Expired or invalid OAuth token, missing
Authorizationheader, or incorrect client credentials. - Fix: Verify
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRETmatch the registered app. Ensure the token request usesapplication/x-www-form-urlencodedencoding. CallgetAuthToken()before every SDK operation. - Code: Add token validation retry before API calls. Check
response.status === 401and re-authenticate.
Error: 403 Forbidden
- Cause: OAuth app lacks required scopes, or the user account associated with the app does not have system administrator or wrap-up code management permissions.
- Fix: Grant
wrapupcodes:write,routing:queue:write,analytics:conversations:read, andplatform:log:readin the Genesys Cloud admin console under Apps > OAuth. Assign the app to a user with the appropriate role.
Error: 409 Conflict
- Cause: Duplicate
wrapupCodeidentifier or ETag mismatch during queue updates. - Fix: Use
GET /api/v2/wrapupcodesto search existing codes before creation. For ETag conflicts, fetch the latest version, merge changes, and retry withIf-Match: <etag>. - Code: Implement the
pollQueueActivationETag check. Always read the current state beforePUToperations.
Error: 429 Too Many Requests
- Cause: Exceeding Genesys Cloud rate limits (typically 100-300 requests per second depending on endpoint).
- Fix: Implement exponential backoff with jitter. The
retryOn429wrapper handles this automatically. Reduce batch sizes for analytics and log queries.
Error: 5xx Server Error
- Cause: Temporary platform outage or internal routing failure.
- Fix: Retry with exponential backoff. If persistent, check Genesys Cloud status page. Log request IDs from response headers (
x-request-id) for support tickets.