Automating NICE CXone Outbound Disposition Code Management with Node.js
What You Will Build
- A Node.js script that programmatically retrieves existing disposition codes, creates hierarchical outcome structures, assigns them to campaigns, enforces compliance validation rules, updates agent desktop visibility settings, and synchronizes definitions across campaign groups.
- This tutorial uses the NICE CXone REST API v2 endpoints for outcomes and campaigns.
- The implementation covers Node.js 18+ using
axiosfor HTTP requests anddotenvfor environment configuration.
Prerequisites
- OAuth 2.0 Client Credentials grant configured in CXone Admin
- Required scopes:
outcomes:read,outcomes:write,campaigns:read,campaigns:write,desktop:write - Node.js 18 or newer
- External dependencies:
axios,dotenv - Tenant base URL (e.g.,
https://api-us-01.nice-incontact.comorhttps://api-eu-01.nice-incontact.com)
Authentication Setup
NICE CXone uses a standard OAuth 2.0 Client Credentials flow. The token endpoint returns a bearer token valid for one hour. Production code must cache the token and handle expiration.
const axios = require('axios');
require('dotenv').config();
const CXONE_BASE = process.env.CXONE_BASE_URL;
const CLIENT_ID = process.env.CXONE_CLIENT_ID;
const CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET;
let accessToken = null;
let tokenExpiry = 0;
async function getAccessToken() {
if (accessToken && Date.now() < tokenExpiry - 60000) {
return accessToken;
}
const response = await axios.post(
`${CXONE_BASE}/api/v2/oauth/token`,
new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'outcomes:read outcomes:write campaigns:read campaigns:write desktop:write'
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
accessToken = response.data.access_token;
tokenExpiry = Date.now() + (response.data.expires_in * 1000);
return accessToken;
}
// Retry wrapper for 429 rate limits
async function cxoneRequest(method, url, options = {}) {
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
const token = await getAccessToken();
const response = await axios({
method,
url,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
},
data: options.data,
params: options.params
});
return response;
} catch (error) {
if (error.response?.status === 429 && attempt < maxRetries - 1) {
const retryAfter = error.response.headers['retry-after'] || Math.pow(2, attempt);
console.log(`Rate limited. Retrying in ${retryAfter} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
attempt++;
continue;
}
throw error;
}
}
}
Implementation
Step 1: Fetch Existing Dispositions via the Campaign API
The /api/v2/outcomes endpoint returns all disposition codes. CXone supports pagination using page and pageSize. The required scope is outcomes:read.
async function fetchAllOutcomes() {
const allOutcomes = [];
let page = 1;
const pageSize = 250;
while (true) {
const response = await cxoneRequest('GET', `${CXONE_BASE}/api/v2/outcomes`, {
params: { page, pageSize }
});
const outcomes = response.data?.items || [];
allOutcomes.push(...outcomes);
if (!response.data?.nextPage || outcomes.length < pageSize) {
break;
}
page++;
}
return allOutcomes;
}
The response structure contains an array of outcome objects with fields like id, name, code, parentOutcomeId, and status. You must handle empty arrays and missing pagination tokens gracefully.
Step 2: Create New Disposition Codes with Hierarchical Structures
Hierarchical dispositions require a parentOutcomeId reference. The parent must exist before creating children. The required scope is outcomes:write.
async function createHierarchicalOutcomes(outcomes) {
const created = [];
const lookup = new Map();
for (const outcome of outcomes) {
const payload = {
name: outcome.name,
code: outcome.code,
description: outcome.description || '',
status: 'ACTIVE',
parentOutcomeId: outcome.parentId ? lookup.get(outcome.parentId) : null
};
const response = await cxoneRequest('POST', `${CXONE_BASE}/api/v2/outcomes`, {
data: payload
});
const newId = response.data.id;
lookup.set(outcome.id, newId);
created.push({ ...outcome, cxoneId: newId });
}
return created;
}
The parentOutcomeId field establishes the tree structure. If you attempt to create a child before the parent, the API returns a 400 Bad Request. The lookup map ensures correct ID mapping during batch creation.
Step 3: Map Dispositions to Campaign Outcomes
Campaigns link to outcomes via the /api/v2/campaigns/{campaignId}/outcomes endpoint. This assignment determines which disposition codes appear in the agent desktop during outbound calls. The required scope is campaigns:write.
async function mapOutcomesToCampaign(campaignId, outcomeIds) {
const mappings = [];
for (const outcomeId of outcomeIds) {
const response = await cxoneRequest('POST', `${CXONE_BASE}/api/v2/campaigns/${campaignId}/outcomes`, {
data: {
outcomeId: outcomeId,
sequence: mappings.length + 1,
defaultSelection: false
}
});
mappings.push(response.data);
}
return mappings;
}
The sequence parameter controls the display order in the agent interface. Setting defaultSelection to true automatically selects the disposition when the agent opens the outcome panel.
Step 4: Validate Code Usage Rules Against Compliance Requirements
Compliance validation is enforced through outcome properties. You update the outcome definition with validationRules that specify mandatory fields, retention periods, and regulatory tags. The required scope is outcomes:write.
async function applyComplianceRules(outcomeId, complianceConfig) {
const validationPayload = {
id: outcomeId,
validationRules: {
requiredFields: complianceConfig.requiredFields || [],
retentionPeriodDays: complianceConfig.retentionDays || 365,
complianceCategory: complianceConfig.category || 'INTERNAL',
blockDisqualification: complianceConfig.blockDisqual || false,
mandatoryReasonCode: complianceConfig.reasonCode || null
}
};
const response = await cxoneRequest('PUT', `${CXONE_BASE}/api/v2/outcomes/${outcomeId}`, {
data: validationPayload
});
return response.data;
}
The blockDisqualification flag prevents agents from marking a contact as uncallable when this disposition is selected. The complianceCategory field triggers audit logging in the CXone compliance dashboard.
Step 5: Update Agent Desktop Configurations to Reflect New Codes
Desktop visibility and grouping are managed via the outcome UI configuration endpoint. This step ensures new dispositions appear in the correct agent desktop sections. The required scope is desktop:write.
async function updateDesktopConfig(outcomeId, desktopSettings) {
const configPayload = {
uiConfig: {
visible: desktopSettings.visible !== false,
group: desktopSettings.group || 'STANDARD',
colorCode: desktopSettings.color || '#4A90E2',
icon: desktopSettings.icon || 'default',
shortcutKey: desktopSettings.shortcut || null
}
};
const response = await cxoneRequest('PATCH', `${CXONE_BASE}/api/v2/outcomes/${outcomeId}/desktop-config`, {
data: configPayload
});
return response.data;
}
The group parameter maps to desktop outcome categories. Agents only see outcomes assigned to their role-based desktop configuration. The colorCode field renders a visual indicator in the outcome dropdown.
Step 6: Synchronize Disposition Definitions Across Multiple Campaign Groups
Synchronization requires fetching campaign groups, iterating through member campaigns, and applying the outcome mappings. This step uses campaigns:read and campaigns:write.
async function synchronizeAcrossCampaignGroups(groupIds, outcomeMappings) {
const syncResults = [];
for (const groupId of groupIds) {
const groupResponse = await cxoneRequest('GET', `${CXONE_BASE}/api/v2/campaign-groups/${groupId}`);
const campaignIds = groupResponse.data.campaignIds || [];
for (const campaignId of campaignIds) {
const mappings = await mapOutcomesToCampaign(campaignId, outcomeMappings.map(m => m.cxoneId));
syncResults.push({ campaignId, mappedCount: mappings.length });
}
}
return syncResults;
}
The API returns a flat list of campaign IDs per group. You must handle campaigns that already contain overlapping outcomes to prevent duplicate mappings. The CXone API rejects duplicate outcome assignments with a 409 Conflict.
Complete Working Example
const axios = require('axios');
require('dotenv').config();
const CXONE_BASE = process.env.CXONE_BASE_URL;
const CLIENT_ID = process.env.CXONE_CLIENT_ID;
const CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET;
let accessToken = null;
let tokenExpiry = 0;
async function getAccessToken() {
if (accessToken && Date.now() < tokenExpiry - 60000) {
return accessToken;
}
const response = await axios.post(
`${CXONE_BASE}/api/v2/oauth/token`,
new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'outcomes:read outcomes:write campaigns:read campaigns:write desktop:write'
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
accessToken = response.data.access_token;
tokenExpiry = Date.now() + (response.data.expires_in * 1000);
return accessToken;
}
async function cxoneRequest(method, url, options = {}) {
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
const token = await getAccessToken();
const response = await axios({
method,
url,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
},
data: options.data,
params: options.params
});
return response;
} catch (error) {
if (error.response?.status === 429 && attempt < maxRetries - 1) {
const retryAfter = error.response.headers['retry-after'] || Math.pow(2, attempt);
console.log(`Rate limited. Retrying in ${retryAfter} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
attempt++;
continue;
}
throw error;
}
}
}
async function fetchAllOutcomes() {
const allOutcomes = [];
let page = 1;
const pageSize = 250;
while (true) {
const response = await cxoneRequest('GET', `${CXONE_BASE}/api/v2/outcomes`, {
params: { page, pageSize }
});
const outcomes = response.data?.items || [];
allOutcomes.push(...outcomes);
if (!response.data?.nextPage || outcomes.length < pageSize) {
break;
}
page++;
}
return allOutcomes;
}
async function createHierarchicalOutcomes(outcomes) {
const created = [];
const lookup = new Map();
for (const outcome of outcomes) {
const payload = {
name: outcome.name,
code: outcome.code,
description: outcome.description || '',
status: 'ACTIVE',
parentOutcomeId: outcome.parentId ? lookup.get(outcome.parentId) : null
};
const response = await cxoneRequest('POST', `${CXONE_BASE}/api/v2/outcomes`, {
data: payload
});
const newId = response.data.id;
lookup.set(outcome.id, newId);
created.push({ ...outcome, cxoneId: newId });
}
return created;
}
async function mapOutcomesToCampaign(campaignId, outcomeIds) {
const mappings = [];
for (const outcomeId of outcomeIds) {
const response = await cxoneRequest('POST', `${CXONE_BASE}/api/v2/campaigns/${campaignId}/outcomes`, {
data: {
outcomeId: outcomeId,
sequence: mappings.length + 1,
defaultSelection: false
}
});
mappings.push(response.data);
}
return mappings;
}
async function applyComplianceRules(outcomeId, complianceConfig) {
const validationPayload = {
id: outcomeId,
validationRules: {
requiredFields: complianceConfig.requiredFields || [],
retentionPeriodDays: complianceConfig.retentionDays || 365,
complianceCategory: complianceConfig.category || 'INTERNAL',
blockDisqualification: complianceConfig.blockDisqual || false,
mandatoryReasonCode: complianceConfig.reasonCode || null
}
};
const response = await cxoneRequest('PUT', `${CXONE_BASE}/api/v2/outcomes/${outcomeId}`, {
data: validationPayload
});
return response.data;
}
async function updateDesktopConfig(outcomeId, desktopSettings) {
const configPayload = {
uiConfig: {
visible: desktopSettings.visible !== false,
group: desktopSettings.group || 'STANDARD',
colorCode: desktopSettings.color || '#4A90E2',
icon: desktopSettings.icon || 'default',
shortcutKey: desktopSettings.shortcut || null
}
};
const response = await cxoneRequest('PATCH', `${CXONE_BASE}/api/v2/outcomes/${outcomeId}/desktop-config`, {
data: configPayload
});
return response.data;
}
async function synchronizeAcrossCampaignGroups(groupIds, outcomeMappings) {
const syncResults = [];
for (const groupId of groupIds) {
const groupResponse = await cxoneRequest('GET', `${CXONE_BASE}/api/v2/campaign-groups/${groupId}`);
const campaignIds = groupResponse.data.campaignIds || [];
for (const campaignId of campaignIds) {
const mappings = await mapOutcomesToCampaign(campaignId, outcomeMappings.map(m => m.cxoneId));
syncResults.push({ campaignId, mappedCount: mappings.length });
}
}
return syncResults;
}
async function main() {
try {
console.log('Fetching existing outcomes...');
const existing = await fetchAllOutcomes();
console.log(`Found ${existing.length} existing outcomes.`);
const newOutcomes = [
{ id: 'PARENT_01', name: 'Sales Inquiry', code: 'SI', parentId: null },
{ id: 'CHILD_01A', name: 'Product Demo Requested', code: 'SI-DEMO', parentId: 'PARENT_01' },
{ id: 'CHILD_01B', name: 'Pricing Quote Sent', code: 'SI-QUOTE', parentId: 'PARENT_01' }
];
console.log('Creating hierarchical outcomes...');
const created = await createHierarchicalOutcomes(newOutcomes);
console.log('Applying compliance rules...');
for (const outcome of created) {
await applyComplianceRules(outcome.cxoneId, {
category: 'GDPR',
retentionDays: 730,
requiredFields: ['agent_notes', 'contact_consent'],
blockDisqual: true,
reasonCode: 'OUTBOUND_SALES'
});
}
console.log('Updating desktop configurations...');
for (const outcome of created) {
await updateDesktopConfig(outcome.cxoneId, {
visible: true,
group: 'SALES_OUTBOUND',
color: '#2ECC71',
icon: 'phone-forwarded'
});
}
console.log('Synchronizing across campaign groups...');
const groupIds = ['GRP_SALES_EAST', 'GRP_SALES_WEST'];
const results = await synchronizeAcrossCampaignGroups(groupIds, created);
console.log('Synchronization complete:', results);
} catch (error) {
console.error('Workflow failed:', error.response?.data || error.message);
process.exit(1);
}
}
main();
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token has expired or the client credentials are incorrect.
- Fix: Verify
CXONE_CLIENT_IDandCXONE_CLIENT_SECRETin your environment. Ensure the token refresh logic runs before each request batch. ThegetAccessTokenfunction includes a 60-second buffer to prevent mid-request expiration.
Error: 403 Forbidden
- Cause: The OAuth client lacks the required scopes for the targeted endpoint.
- Fix: Update the client application in CXone Admin to include
outcomes:writeandcampaigns:write. The token request must explicitly list all scopes. Missing scopes cause silent failures in mapping operations.
Error: 409 Conflict
- Cause: Attempting to assign an outcome to a campaign where it already exists.
- Fix: Query existing campaign outcomes before mapping. Filter the
outcomeIdsarray against the campaign’s current assignment list. The API rejects duplicate POST requests to/api/v2/campaigns/{id}/outcomes.
Error: 429 Too Many Requests
- Cause: Exceeding the tenant rate limit, typically 100 requests per second for outcome operations.
- Fix: The
cxoneRequestwrapper implements exponential backoff. For bulk operations, introduce a 100-millisecond delay between sequential POST calls. Monitor theRetry-Afterheader for precise wait times.
Error: 400 Bad Request on Hierarchical Creation
- Cause: A child outcome references a
parentOutcomeIdthat does not exist or was created in the same batch before the parent ID was resolved. - Fix: Sort outcomes by depth before creation. The lookup map in
createHierarchicalOutcomesresolves parent IDs in execution order. Ensure parent objects appear before children in the input array.