Routing Cognigy Bot Intent Outputs to NICE CXone Queues via Webhook Integration

Routing Cognigy Bot Intent Outputs to NICE CXone Queues via Webhook Integration

What You Will Build

  • You will build a middleware service that intercepts webhook payloads from a NICE Cognigy bot, extracts the detected intent, and dynamically routes the conversation to the appropriate NICE CXone queue using the PureCloud API.
  • This solution uses the NICE Cognigy Webhook feature to send context to your server and the NICE CXone POST /api/v2/conversations endpoint to transfer the interaction.
  • The tutorial covers implementation in Node.js using the express framework and the official NICE CXone JavaScript SDK.

Prerequisites

  • NICE Cognigy Account: A running bot with a configured Webhook action.
  • NICE CXone Account: An active tenant with at least two distinct Queues (e.g., “Sales” and “Support”) and associated Skills.
  • OAuth Credentials: A NICE CXone OAuth Client ID and Client Secret with the scope conversation:write and routing:write.
  • Runtime: Node.js 18+ installed.
  • Dependencies: express, @nice-dev/nice-cxone-sdk, axios, dotenv.

Authentication Setup

NICE CXone requires OAuth 2.0 Client Credentials flow for server-to-server API calls. Because your middleware will be making API calls on behalf of the application (not a specific user), you must cache the access token to avoid hitting rate limits with repeated token requests.

The following code sets up a token cache mechanism. This is critical because the CXone API returns 429 (Too Many Requests) errors if you request a new token for every conversation transfer.

// auth.js
const axios = require('axios');

const CXONE_BASE_URL = process.env.CXONE_BASE_URL || 'https://api.mypurecloud.com';
const CLIENT_ID = process.env.CXONE_CLIENT_ID;
const CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET;

let cachedToken = null;
let tokenExpiry = 0;

/**
 * Retrieves an OAuth2 access token from NICE CXone.
 * Caches the token for its validity period minus a 60-second buffer.
 */
async function getAccessToken() {
  const now = Date.now();

  // Return cached token if it is still valid
  if (cachedToken && now < tokenExpiry) {
    return cachedToken;
  }

  try {
    const response = await axios.post(`${CXONE_BASE_URL}/oauth/token`, {
      grant_type: 'client_credentials',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      scope: 'conversation:write routing:write'
    }, {
      headers: {
        'Content-Type': 'application/json'
      }
    });

    const { access_token, expires_in } = response.data;

    // Cache the token. Subtract 60 seconds to ensure we refresh before expiry.
    cachedToken = access_token;
    tokenExpiry = now + (expires_in * 1000) - 60000;

    return access_token;
  } catch (error) {
    console.error('Failed to retrieve CXone Access Token:', error.response?.data || error.message);
    throw new Error('Authentication failed with NICE CXone');
  }
}

module.exports = { getAccessToken };

Implementation

Step 1: Define the Routing Logic and Queue Mapping

Before handling the webhook, you must define how Cognigy intents map to CXone Queue IDs. CXone routing relies on Queue IDs, not Queue Names. You must retrieve these IDs from your CXone tenant.

Create a configuration object that maps the intent string received from Cognigy to the corresponding CXone queueId.

// config.js

/**
 * Maps Cognigy Intent names to NICE CXone Queue IDs.
 * Replace these IDs with actual Queue IDs from your CXone tenant.
 */
const INTENT_TO_QUEUE_MAP = {
  'sales_inquiry': 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
  'technical_support': 'b2c3d4e5-6789-01bc-defg-234567890abc',
  'billing_question': 'c3d4e5f6-7890-12cd-efgh-34567890abcd',
  'default': 'd4e5f6g7-8901-23de-fghi-4567890abcde' // Fallback queue
};

/**
 * Retrieves the Queue ID for a given intent.
 * Returns the default queue ID if the intent is not found.
 */
function getQueueIdForIntent(intentName) {
  // Normalize intent name to lowercase to handle case-insensitivity
  const normalizedIntent = intentName.toLowerCase().trim();
  return INTENT_TO_QUEUE_MAP[normalizedIntent] || INTENT_TO_QUEUE_MAP['default'];
}

module.exports = { getQueueIdForIntent, INTENT_TO_QUEUE_MAP };

Step 2: Construct the Webhook Listener

Your Express server must expose an endpoint that NICE Cognigy can call. When the bot detects an intent, it triggers a Webhook action. This action sends a JSON payload containing the sessionId, userId, and the intent.

The middleware must validate this payload and prepare the data for the CXone API.

// server.js
const express = require('express');
const { getAccessToken } = require('./auth');
const { getQueueIdForIntent } = require('./config');

const app = express();
app.use(express.json()); // Parse incoming JSON payloads

// Endpoint for NICE Cognigy Webhook
app.post('/api/cognigy/route', async (req, res) => {
  try {
    const { sessionId, userId, intent, variables } = req.body;

    // 1. Validate Input
    if (!sessionId || !userId || !intent) {
      return res.status(400).json({ error: 'Missing required fields: sessionId, userId, intent' });
    }

    console.log(`Received routing request for Session: ${sessionId}, Intent: ${intent}`);

    // 2. Determine Target Queue
    const targetQueueId = getQueueIdForIntent(intent);
    console.log(`Routing to Queue ID: ${targetQueueId}`);

    // 3. Perform the CXone Transfer (Implemented in Step 3)
    await routeConversationToCXone(sessionId, userId, targetQueueId, intent);

    // 4. Respond to Cognigy
    // Return success to Cognigy so the bot flow continues or ends as configured
    res.status(200).json({
      status: 'success',
      message: `Conversation routed to queue: ${targetQueueId}`,
      routedIntent: intent
    });

  } catch (error) {
    console.error('Error processing Cognigy webhook:', error);
    res.status(500).json({ error: 'Internal server error during routing' });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Cognigy-CXone Routing Middleware running on port ${PORT}`);
});

Step 3: Execute the CXone Conversation Transfer

This is the core logic. You must use the NICE CXone API to create a new conversation leg or transfer an existing one. Since Cognigy often acts as a “Digital” channel, the most robust pattern is to create a new “Queue” conversation in CXone that references the original digital session, or to use the POST /api/v2/conversations endpoint with a routingData object.

For this tutorial, we will use the standard POST /api/v2/conversations endpoint to initiate a queued interaction. This assumes the user is being handed off from the bot to a human agent.

Critical Note: The from and to objects in the CXone API require valid participant objects. For a digital handoff, the from participant is typically the user (identified by their Cognigy userId), and the to participant is the Queue.

// cxoneClient.js
const axios = require('axios');
const { getAccessToken } = require('./auth');

const CXONE_BASE_URL = process.env.CXONE_BASE_URL || 'https://api.mypurecloud.com';

/**
 * Routes a conversation to a specific NICE CXone Queue.
 * 
 * @param {string} sessionId - The Cognigy Session ID.
 * @param {string} userId - The unique user identifier from Cognigy.
 * @param {string} queueId - The CXone Queue ID to route to.
 * @param {string} intent - The detected intent, passed as a variable for agent context.
 */
async function routeConversationToCXone(sessionId, userId, queueId, intent) {
  const token = await getAccessToken();

  // Construct the conversation payload
  // We create a "Queue" type conversation.
  // The 'from' object represents the customer.
  // The 'to' object represents the destination (Queue).
  const payload = {
    type: 'Queue',
    from: {
      id: userId,
      name: `User_${userId}`, // Optional: Use a real name if available in Cognigy variables
      address: `cognigy://${userId}`, // Custom address scheme to identify source
      type: 'Person'
    },
    to: [
      {
        id: queueId,
        type: 'Queue'
      }
    ],
    routingData: {
      priority: 5, // 1-5, where 5 is highest priority
      skills: [] // Optional: Add specific skills if the queue requires them
    },
    // Pass the intent and session info as variables for the agent to see
    variables: [
      {
        name: 'cognigySessionId',
        value: sessionId,
        valueType: 'STRING'
      },
      {
        name: 'detectedIntent',
        value: intent,
        valueType: 'STRING'
      }
    ]
  };

  try {
    const response = await axios.post(
      `${CXONE_BASE_URL}/api/v2/conversations`,
      payload,
      {
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      }
    );

    console.log(`Successfully created CXone conversation: ${response.data.conversations[0].id}`);
    return response.data;

  } catch (error) {
    if (error.response) {
      // Handle specific CXone API errors
      if (error.response.status === 401 || error.response.status === 403) {
        console.error('CXone API Error: Unauthorized. Check OAuth scopes.');
        throw new Error('CXone Authentication Failed');
      }
      if (error.response.status === 429) {
        console.error('CXone API Error: Rate Limited. Implement retry logic.');
        throw new Error('CXone Rate Limit Exceeded');
      }
      console.error('CXone API Error:', error.response.data);
      throw new Error(`CXone API Error: ${error.response.status} ${error.response.statusText}`);
    } else {
      console.error('CXone API Request Failed:', error.message);
      throw new Error('CXone API Request Failed');
    }
  }
}

module.exports = { routeConversationToCXone };

Complete Working Example

Below is the consolidated, runnable Node.js application. Save this as index.js and ensure you have a .env file with your credentials.

// index.js
require('dotenv').config();
const express = require('express');
const axios = require('axios');

// --- Configuration ---
const CXONE_BASE_URL = process.env.CXONE_BASE_URL || 'https://api.mypurecloud.com';
const CLIENT_ID = process.env.CXONE_CLIENT_ID;
const CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET;

const INTENT_TO_QUEUE_MAP = {
  'sales': 'QUEUE_ID_SALES',
  'support': 'QUEUE_ID_SUPPORT',
  'default': 'QUEUE_ID_DEFAULT'
};

// --- Token Cache ---
let cachedToken = null;
let tokenExpiry = 0;

async function getAccessToken() {
  const now = Date.now();
  if (cachedToken && now < tokenExpiry) return cachedToken;

  try {
    const res = await axios.post(`${CXONE_BASE_URL}/oauth/token`, {
      grant_type: 'client_credentials',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      scope: 'conversation:write routing:write'
    });
    cachedToken = res.data.access_token;
    tokenExpiry = now + (res.data.expires_in * 1000) - 60000;
    return cachedToken;
  } catch (e) {
    throw new Error('Auth Failed');
  }
}

// --- Routing Logic ---
async function routeToCXone(sessionId, userId, queueId, intent) {
  const token = await getAccessToken();
  const payload = {
    type: 'Queue',
    from: { id: userId, name: `User_${userId}`, address: `cognigy://${userId}`, type: 'Person' },
    to: [{ id: queueId, type: 'Queue' }],
    routingData: { priority: 5 },
    variables: [
      { name: 'cognigySessionId', value: sessionId, valueType: 'STRING' },
      { name: 'detectedIntent', value: intent, valueType: 'STRING' }
    ]
  };

  const res = await axios.post(`${CXONE_BASE_URL}/api/v2/conversations`, payload, {
    headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
  });
  return res.data;
}

// --- Express Server ---
const app = express();
app.use(express.json());

app.post('/api/route', async (req, res) => {
  try {
    const { sessionId, userId, intent } = req.body;
    if (!sessionId || !userId || !intent) {
      return res.status(400).json({ error: 'Missing fields' });
    }

    const queueId = INTENT_TO_QUEUE_MAP[intent.toLowerCase()] || INTENT_TO_QUEUE_MAP['default'];
    await routeToCXone(sessionId, userId, queueId, intent);

    res.json({ status: 'success', queueId });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Routing failed' });
  }
});

app.listen(3000, () => console.log('Middleware running on port 3000'));

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token is invalid, expired, or the Client ID/Secret is incorrect.
  • Fix: Verify that your .env file contains the correct CXONE_CLIENT_ID and CXONE_CLIENT_SECRET. Ensure the OAuth Client in CXone is enabled and has the conversation:write scope assigned.
  • Debugging: Log the raw response from the /oauth/token endpoint. If it returns an error, the credentials are wrong. If it returns a token but the subsequent API call fails, the token may have expired (check your cache logic).

Error: 403 Forbidden

  • Cause: The OAuth Client lacks the required permissions.
  • Fix: Go to the NICE CXone Admin Portal > Security > OAuth Clients. Edit your client and ensure the following scopes are checked:
    • conversation:write
    • routing:write
  • Debugging: Confirm that the user associated with the OAuth client (if using User Credentials flow) has the necessary role permissions. For Client Credentials flow, only the client scopes matter.

Error: 429 Too Many Requests

  • Cause: You are exceeding the NICE CXone API rate limits. This often happens if you request a new OAuth token for every single conversation.
  • Fix: Implement token caching as shown in the getAccessToken function. Do not call /oauth/token more than once every 50 minutes (tokens are valid for 1 hour).
  • Debugging: Check your logs for repeated POST /oauth/token calls. If you see them, your cache is not persisting correctly.

Error: 400 Bad Request (Invalid Queue ID)

  • Cause: The queueId sent to CXone does not exist or is not a valid UUID.
  • Fix: Verify the Queue IDs in your INTENT_TO_QUEUE_MAP. You can find these IDs in the CXone Admin Portal under Routing > Queues. Copy the ID from the URL or the API response of GET /api/v2/routing/queues.
  • Debugging: Log the queueId being used in the routeToCXone function to ensure it matches a known valid ID.

Official References