Routing Cognigy Intent Results to NICE CXone via Webhooks and API
What You Will Build
- One sentence: The code extracts intent confidence scores from a NICE Cognigy webhook payload and updates the active conversation context in NICE CXone using the Interaction API.
- One sentence: This uses the NICE CXone Interaction API (
/api/v2/interactions/interactions) and standard HTTP POST webhooks. - One sentence: The implementation covers JavaScript (Node.js) for the webhook server and Python for the CXone API integration.
Prerequisites
- NICE CXone Environment: An active CXone instance with API access.
- OAuth Credentials: A CXone Client ID, Client Secret, and Tenant Domain for generating access tokens.
- Cognigy Platform: A configured Cognigy Studio project with a webhook node or custom integration node.
- Node.js: Version 18+ for the webhook receiver.
- Python: Version 3.9+ for the CXone API client logic.
- Dependencies:
- Node.js:
express,axios,cors - Python:
requests,cxone-sdk(optional, but we will userequestsfor explicit control)
- Node.js:
Authentication Setup
NICE CXone requires OAuth 2.0 Client Credentials flow for server-to-server API calls. You must generate a valid access token before making any Interaction API calls.
Required Scopes:
interaction:read(to get conversation details)interaction:write(to update conversation context or route)routing:write(if modifying routing configuration directly, though usually context updates suffice)
Python Token Generator
Create a utility function to handle token acquisition and caching. This prevents hitting rate limits by requesting a new token for every webhook event.
import requests
import time
from typing import Optional
class CXoneAuth:
def __init__(self, tenant_domain: str, client_id: str, client_secret: str):
self.tenant_domain = tenant_domain
self.client_id = client_id
self.client_secret = client_secret
self.token_url = f"https://{tenant_domain}/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0
def get_access_token(self) -> str:
# Return cached token if still valid (subtract 60s for safety buffer)
if self.access_token and time.time() < self.token_expiry - 60:
return self.access_token
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(self.token_url, headers=headers, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
# Expires_in is in seconds
self.token_expiry = time.time() + token_data["expires_in"]
return self.access_token
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("Invalid CXone Client ID or Secret") from e
raise Exception(f"Failed to obtain CXone token: {response.text}") from e
except requests.exceptions.RequestException as e:
raise Exception(f"Network error obtaining CXone token: {str(e)}") from e
Implementation
Step 1: Configure Cognigy Webhook Output
In Cognigy Studio, you do not “configure” the payload structure via an API. Instead, you define it in your JavaScript/TypeScript code within the Node.js node or Custom Integration.
The goal is to send a JSON payload that maps Cognigy intents to a format CXone can consume. CXone relies on Conversation Context attributes for routing.
Cognigy Node Logic (JavaScript):
// Inside Cognigy Studio Node.js Node
const axios = require('axios');
module.exports = async (session) => {
try {
// 1. Get the detected intent and confidence
const intent = session.nlpResult?.intent;
const confidence = session.nlpResult?.confidence;
if (!intent) {
session.setVariable('routingError', 'No intent detected');
return;
}
// 2. Construct the payload for CXone
// We include the CXone Conversation ID which Cognigy must receive
// from the initial channel connection (e.g., via Chat API or Voice)
const cxoneConversationId = session.getVariable('cxoneConversationId');
if (!cxoneConversationId) {
throw new Error("Missing CXone Conversation ID in session variables");
}
const payload = {
conversationId: cxoneConversationId,
intent: intent.name,
confidence: intent.confidence,
timestamp: new Date().toISOString()
};
// 3. Send to our intermediate webhook server (Node.js)
// Directly calling CXone from Cognigy is possible but less flexible for retry logic
await axios.post('https://your-webhook-server.com/api/route', payload, {
headers: { 'Content-Type': 'application/json' }
});
// 4. Set a success flag in Cognigy for subsequent nodes
session.setVariable('routingSuccess', true);
session.setVariable('detectedIntent', intent.name);
} catch (error) {
console.error("Routing Error:", error.message);
session.setVariable('routingSuccess', false);
session.setVariable('routingErrorMessage', error.message);
}
};
Step 2: Build the Webhook Receiver and Router (Node.js)
This server receives the intent data from Cognigy and translates it into a CXone API call. This decoupling allows you to handle retries, logging, and complex routing logic without blocking the Cognigy execution thread.
File: server.js
const express = require('express');
const cors = require('cors');
const axios = require('axios');
const CXoneAuth = require('./cxoneAuth'); // Assuming the Python logic is ported to Node or we use a separate microservice
// For this tutorial, we will implement the Node.js equivalent of the auth logic inline for completeness
class NodeCXoneAuth {
constructor(tenantDomain, clientId, clientSecret) {
this.tenantDomain = tenantDomain;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.tokenUrl = `https://${tenantDomain}/oauth/token`;
this.accessToken = null;
this.tokenExpiry = 0;
}
async getAccessToken() {
if (this.accessToken && Date.now() < this.tokenExpiry - 60000) {
return this.accessToken;
}
const formData = new URLSearchParams();
formData.append('grant_type', 'client_credentials');
formData.append('client_id', this.clientId);
formData.append('client_secret', this.clientSecret);
try {
const response = await axios.post(this.tokenUrl, formData, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
this.accessToken = response.data.access_token;
this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
return this.accessToken;
} catch (error) {
throw new Error(`CXone Auth Failed: ${error.response?.data || error.message}`);
}
}
}
const app = express();
app.use(cors());
app.use(express.json());
// Configuration
const CXONE_TENANT = process.env.CXONE_TENANT || 'demo';
const CXONE_CLIENT_ID = process.env.CXONE_CLIENT_ID || 'your-client-id';
const CXONE_CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET || 'your-client-secret';
const cxoneAuth = new NodeCXoneAuth(CXONE_TENANT, CXONE_CLIENT_ID, CXONE_CLIENT_SECRET);
// Step 2: Core Logic - Update Conversation Context
app.post('/api/route', async (req, res) => {
const { conversationId, intent, confidence } = req.body;
if (!conversationId || !intent) {
return res.status(400).json({ error: 'Missing conversationId or intent' });
}
try {
const token = await cxoneAuth.getAccessToken();
// CXone API: Update Conversation Context
// Endpoint: POST /api/v2/interactions/interactions/{id}/context
// Scope: interaction:write
const contextPayload = {
attributes: {
// Custom attribute for routing
'cognigyDetectedIntent': intent,
'cognigyIntentConfidence': confidence,
'lastUpdated': new Date().toISOString()
}
};
const apiResponse = await axios.post(
`https://${CXONE_TENANT}.my.cxone.com/api/v2/interactions/interactions/${conversationId}/context`,
contextPayload,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log(`Successfully updated context for conversation ${conversationId} with intent: ${intent}`);
// Optional: If you need to force a re-route, you might need to update the interaction's routing state
// However, simply updating context is usually sufficient if your CXone Routing Rules are set to
// "Re-evaluate on Context Change" or if the interaction is currently in a "Waiting" state.
res.status(200).json({ success: true, data: apiResponse.data });
} catch (error) {
console.error("CXone API Error:", error.response?.data || error.message);
// Handle specific CXone errors
if (error.response?.status === 429) {
// Retry logic would go here in production
return res.status(429).json({ error: 'Rate limited by CXone' });
}
if (error.response?.status === 404) {
return res.status(404).json({ error: 'Conversation not found in CXone' });
}
res.status(500).json({ error: 'Failed to update CXone context' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Webhook server running on port ${PORT}`);
});
Step 3: Configuring CXone Routing Rules
The API call above updates the Context of the conversation. For this to result in dynamic routing, you must configure a Routing Rule in NICE CXone.
While the prompt asks for code, the “code” for routing rules is often done via the UI. However, you can manage this via the Routing API if you need to deploy these rules as code.
Python Script to Create a Routing Rule:
This script creates a rule that routes interactions with cognigyDetectedIntent equal to “billing” to a specific Queue.
import requests
import json
from cxone_auth import CXoneAuth # Using the class defined in Authentication Setup
def create_routing_rule(auth: CXoneAuth, queue_id: str, intent_value: str):
"""
Creates a CXone Routing Rule that matches on the custom context attribute.
"""
tenant = auth.tenant_domain
token = auth.get_access_token()
url = f"https://{tenant}/api/v2/routing/rules"
# Define the Rule Structure
# We use a 'condition' that checks the context attribute
rule_data = {
"name": f"Cognigy Intent Route: {intent_value}",
"description": f"Routes conversations with intent '{intent_value}' to specific queue",
"priority": 1, # Higher priority rules are evaluated first
"enabled": True,
"conditions": [
{
"type": "attribute",
"attributeName": "cognigyDetectedIntent",
"operator": "equals",
"value": intent_value
}
],
"targets": [
{
"type": "queue",
"queueId": queue_id,
"weight": 1
}
]
}
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
try:
response = requests.post(url, headers=headers, data=json.dumps(rule_data))
response.raise_for_status()
created_rule = response.json()
print(f"Successfully created rule: {created_rule['id']}")
return created_rule
except requests.exceptions.HTTPError as e:
print(f"Failed to create rule: {response.status_code} - {response.text}")
if response.status_code == 409:
print("Rule may already exist. Check for duplicates.")
raise
# Usage Example
if __name__ == "__main__":
auth = CXoneAuth(
tenant_domain="demo",
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET"
)
# Replace with your actual Queue ID from CXone
BILLING_QUEUE_ID = "12345678-1234-1234-1234-123456789012"
create_routing_rule(auth, BILLING_QUEUE_ID, "billing_inquiry")
Complete Working Example
Below is the consolidated Node.js webhook server that handles authentication, payload validation, and CXone context updates.
/**
* Cognigy to CXone Router
*
* Receives intent data from Cognigy and updates the CXone Conversation Context.
* This enables CXone Routing Rules to dynamically route the interaction based on AI-detected intent.
*/
const express = require('express');
const cors = require('cors');
const axios = require('axios');
// --- Configuration ---
const CONFIG = {
CXONE_TENANT: process.env.CXONE_TENANT || 'demo',
CXONE_CLIENT_ID: process.env.CXONE_CLIENT_ID || '',
CXONE_CLIENT_SECRET: process.env.CXONE_CLIENT_SECRET || '',
PORT: process.env.PORT || 3000
};
// --- OAuth Module ---
class CXoneOAuth {
constructor(tenant, clientId, clientSecret) {
this.tenant = tenant;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.tokenUrl = `https://${tenant}/oauth/token`;
this.accessToken = null;
this.expiryTime = 0;
}
async getToken() {
// Check if token is still valid (buffer of 60 seconds)
if (this.accessToken && Date.now() < this.expiryTime - 60000) {
return this.accessToken;
}
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret
});
try {
const response = await axios.post(this.tokenUrl, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
this.accessToken = response.data.access_token;
this.expiryTime = Date.now() + (response.data.expires_in * 1000);
return this.accessToken;
} catch (error) {
console.error("CXone OAuth Error:", error.response?.data || error.message);
throw new Error("Failed to acquire CXone Access Token");
}
}
}
// --- Application Setup ---
const app = express();
app.use(cors());
app.use(express.json());
const oauth = new CXoneOAuth(CONFIG.CXONE_TENANT, CONFIG.CXONE_CLIENT_ID, CONFIG.CXONE_CLIENT_SECRET);
// --- Routes ---
/**
* POST /api/route
*
* Expects JSON body:
* {
* "conversationId": "string (CXone Interaction ID)",
* "intent": "string (Detected Intent Name)",
* "confidence": "number (0-1)"
* }
*/
app.post('/api/route', async (req, res) => {
const { conversationId, intent, confidence } = req.body;
// 1. Validation
if (!conversationId || !intent) {
return res.status(400).json({
success: false,
error: "Missing required fields: conversationId, intent"
});
}
try {
// 2. Get Auth Token
const token = await oauth.getToken();
// 3. Prepare Context Update Payload
// CXone Context API allows updating custom attributes
const contextUpdate = {
attributes: {
cognigyDetectedIntent: intent,
cognigyConfidence: confidence,
routedAt: new Date().toISOString()
}
};
// 4. Call CXone Interaction Context API
const cxoneUrl = `https://${CONFIG.CXONE_TENANT}.my.cxone.com/api/v2/interactions/interactions/${conversationId}/context`;
const apiResponse = await axios.post(cxoneUrl, contextUpdate, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
// 5. Return Success
res.status(200).json({
success: true,
message: `Context updated for conversation ${conversationId}`,
data: apiResponse.data
});
} catch (error) {
// 6. Error Handling
console.error("Routing Error:", error);
let errorMessage = "Unknown error";
if (error.response) {
errorMessage = `CXone API Error ${error.response.status}: ${error.response.data?.errors?.[0]?.message || 'Unknown'}`;
} else if (error.message) {
errorMessage = error.message;
}
res.status(500).json({
success: false,
error: errorMessage
});
}
});
// --- Health Check ---
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// --- Start Server ---
if (require.main === module) {
app.listen(CONFIG.PORT, () => {
console.log(`Cognigy-CXone Router listening on port ${CONFIG.PORT}`);
console.log(`CXone Tenant: ${CONFIG.CXONE_TENANT}`);
});
}
module.exports = app; // Export for testing
Common Errors & Debugging
Error: 401 Unauthorized
- What causes it: The CXone Client ID or Secret is incorrect, or the token has expired and was not refreshed.
- How to fix it: Verify credentials in the CXone Admin Console under Administration > Security > API Clients. Ensure the
grant_typeisclient_credentials. Check the logs to see if the token refresh logic is executing. - Code Fix: Ensure your
CXoneOAuthclass checks theexpiryTimecorrectly.
Error: 403 Forbidden
- What causes it: The OAuth client does not have the required scopes (
interaction:write,interaction:read). - How to fix it: In CXone Admin Console, edit the API Client and ensure the Scopes tab includes:
interaction:readinteraction:writerouting:read(if reading rules)routing:write(if creating rules via API)
Error: 404 Not Found
- What causes it: The
conversationIdprovided by Cognigy does not exist in CXone, or the ID format is incorrect. - How to fix it: Cognigy must receive the CXone Interaction ID from the channel connection (e.g., when the chat is initiated via the CXone Chat Widget API). Ensure this ID is passed into the Cognigy session and then to the webhook.
- Debugging: Log the
conversationIdin the Node.js server before making the API call. Verify it matches the formatxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
Error: 429 Too Many Requests
- What causes it: CXone API rate limits have been exceeded. The default limit is often 100 requests per second for most endpoints, but context updates can be throttled more aggressively if volume is high.
- How to fix it: Implement exponential backoff in your Node.js webhook server.
- Code Snippet for Retry:
async function postWithRetry(url, data, headers, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await axios.post(url, data, { headers });
} catch (error) {
if (error.response?.status === 429 && i < retries - 1) {
const waitTime = Math.pow(2, i) * 1000; // 1s, 2s, 4s
console.log(`Rate limited. Retrying in ${waitTime}ms...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
} else {
throw error;
}
}
}
}