Automating NICE CXone Outbound Disposition Code Management with Node.js

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 axios for HTTP requests and dotenv for 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.com or https://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_ID and CXONE_CLIENT_SECRET in your environment. Ensure the token refresh logic runs before each request batch. The getAccessToken function 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:write and campaigns: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 outcomeIds array 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 cxoneRequest wrapper implements exponential backoff. For bulk operations, introduce a 100-millisecond delay between sequential POST calls. Monitor the Retry-After header for precise wait times.

Error: 400 Bad Request on Hierarchical Creation

  • Cause: A child outcome references a parentOutcomeId that 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 createHierarchicalOutcomes resolves parent IDs in execution order. Ensure parent objects appear before children in the input array.

Official References