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/conversationsendpoint to transfer the interaction. - The tutorial covers implementation in Node.js using the
expressframework 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:writeandrouting: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
.envfile contains the correctCXONE_CLIENT_IDandCXONE_CLIENT_SECRET. Ensure the OAuth Client in CXone is enabled and has theconversation:writescope assigned. - Debugging: Log the raw response from the
/oauth/tokenendpoint. 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:writerouting: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
getAccessTokenfunction. Do not call/oauth/tokenmore than once every 50 minutes (tokens are valid for 1 hour). - Debugging: Check your logs for repeated
POST /oauth/tokencalls. If you see them, your cache is not persisting correctly.
Error: 400 Bad Request (Invalid Queue ID)
- Cause: The
queueIdsent 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 ofGET /api/v2/routing/queues. - Debugging: Log the
queueIdbeing used in therouteToCXonefunction to ensure it matches a known valid ID.