Building Dynamic Intent-Based Routing from Cognigy to NICE CXone
What You Will Build
- This tutorial demonstrates how to extract intent confidence scores from a Cognigy webhook payload and use the NICE CXone API to dynamically route an active conversation to the appropriate skill group.
- This uses the Cognigy Runtime Webhook output and the NICE CXone
/api/v2/conversationsand/api/v2/routing/usersendpoints. - The implementation covers Python for the webhook handler and JavaScript for a client-side validation script.
Prerequisites
- NICE CXone API Client: An OAuth 2.0 Client Credentials client with the following scopes:
conversation:all(to update conversation attributes and routing)routing:all(to query user skills and routing groups)analytics:all(optional, for monitoring)
- Cognigy Studio: A configured project with a “Webhook” node that triggers on intent match.
- Python 3.9+: With
requests,cognigy-sdk, andpydanticinstalled. - Node.js 18+: For the client-side validation example.
Authentication Setup
NICE CXone requires OAuth 2.0 Client Credentials flow for server-to-server communication. You must obtain a short-lived access token before making any API calls.
Python Token Helper
import requests
import time
from typing import Optional
class CxoneAuth:
def __init__(self, env: str, client_id: str, client_secret: str):
"""
Initialize CXone Auth.
env: 'us', 'eu', 'au', etc.
"""
self.base_url = f"https://api.cxone.com" if env == "us" else f"https://{env}.api.cxone.com"
self.client_id = client_id
self.client_secret = client_secret
self.token: Optional[str] = None
self.token_expiry: float = 0
def get_token(self) -> str:
"""Get a fresh token if expired or if no token exists."""
if self.token and time.time() < self.token_expiry:
return self.token
url = f"{self.base_url}/oauth/token"
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "conversation:all routing:all"
}
try:
response = requests.post(url, data=data)
response.raise_for_status()
token_data = response.json()
self.token = token_data["access_token"]
# Tokens typically expire in 3600 seconds. Subtract 30s for buffer.
self.token_expiry = time.time() + (token_data["expires_in"] - 30)
return self.token
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise ValueError("Invalid Client ID or Secret") from e
elif response.status_code == 429:
# Implement backoff logic in production
raise RuntimeError("Rate limited on OAuth endpoint") from e
raise e
Implementation
Step 1: Parse the Cognigy Webhook Payload
Cognigy sends a JSON payload to your webhook URL. The critical fields for routing are the intent name and the score. You must validate this payload to ensure it contains sufficient confidence for routing.
Cognigy Webhook Payload Structure
{
"session": {
"id": "sess_12345",
"user": {
"id": "user_67890",
"name": "John Doe"
},
"cxone": {
"conversationId": "conv_abc123",
"channel": "webchat"
}
},
"intent": {
"name": "billing_inquiry",
"score": 0.92
},
"entities": [
{
"name": "account_number",
"value": "99887766"
}
],
"text": "I want to check my bill"
}
Python Payload Parser
from pydantic import BaseModel, Field
from typing import List, Optional
class CognigyEntity(BaseModel):
name: str
value: str
class CognigyIntent(BaseModel):
name: str
score: float
class CognigySession(BaseModel):
id: str
user: dict
cxone: dict = Field(..., description="Must contain conversationId")
class CognigyWebhookPayload(BaseModel):
session: CognigySession
intent: CognigyIntent
entities: List[CognigyEntity] = []
text: str
def get_conversation_id(self) -> str:
"""Extract the CXone Conversation ID from the session."""
return self.session.cxone.get("conversationId")
def is_confident(self, threshold: float = 0.85) -> bool:
"""Check if intent score meets the routing threshold."""
return self.intent.score >= threshold
Step 2: Map Intents to CXone Routing Skills
You must define a mapping between Cognigy intents and CXone Routing Skills or Groups. This logic resides in your webhook handler.
Intent-to-Skill Mapping Logic
from enum import Enum
class CxoneSkillGroup(str, Enum):
BILLING = "Billing Support"
TECHNICAL = "Technical Support"
SALES = "Sales Team"
DEFAULT = "General Inquiry"
INTENT_TO_SKILL_MAP = {
"billing_inquiry": CxoneSkillGroup.BILLING,
"payment_failure": CxoneSkillGroup.BILLING,
"login_issue": CxoneSkillGroup.TECHNICAL,
"feature_request": CxoneSkillGroup.SALES,
"product_bug": CxoneSkillGroup.TECHNICAL,
}
def determine_target_skill(intent_name: str) -> CxoneSkillGroup:
"""
Map Cognigy intent to CXone Skill Group.
Returns DEFAULT if no match found.
"""
return INTENT_TO_SKILL_MAP.get(intent_name, CxoneSkillGroup.DEFAULT)
Step 3: Update Conversation Routing Attributes
NICE CXone does not automatically route based on external intent names. You must update the conversation’s routingData or attributes and then force a re-routing or assign an agent.
The most robust method is to:
- Update the conversation’s custom attributes with the intent.
- Use the
routingendpoint to assign the conversation to a specificskillGrouporuserGroup.
Update Conversation Routing
import requests
class CxoneConversationManager:
def __init__(self, auth: CxoneAuth):
self.auth = auth
self.base_url = auth.base_url
def update_conversation_routing(
self,
conversation_id: str,
skill_group_name: str,
intent_name: str,
intent_score: float
) -> dict:
"""
Update the conversation with intent data and route to the appropriate skill group.
Requires Scope: conversation:all, routing:all
"""
token = self.auth.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# Step 1: Update Conversation Attributes
# This ensures the agent sees the intent in the CRM/Agent Desktop
attrs_payload = {
"attributes": {
"cognigy": {
"intent": intent_name,
"score": intent_score
}
}
}
attrs_url = f"{self.base_url}/api/v2/conversations/{conversation_id}"
try:
attrs_resp = requests.patch(attrs_url, json=attrs_payload, headers=headers)
attrs_resp.raise_for_status()
except requests.exceptions.HTTPError as e:
# Log but don't fail routing if attribute update fails
print(f"Warning: Failed to update attributes: {e}")
# Step 2: Route to Skill Group
# We use the 'routing' sub-resource to set the target skill
routing_payload = {
"skillGroups": [
{
"name": skill_group_name
}
],
"routingMethod": "longestAvailable" # or 'firstAvailable'
}
routing_url = f"{self.base_url}/api/v2/conversations/{conversation_id}/routing"
try:
routing_resp = requests.patch(routing_url, json=routing_payload, headers=headers)
routing_resp.raise_for_status()
return routing_resp.json()
except requests.exceptions.HTTPError as e:
if routing_resp.status_code == 404:
raise ValueError(f"Conversation {conversation_id} not found") from e
elif routing_resp.status_code == 409:
raise RuntimeError(f"Conversation {conversation_id} is already routed or ended") from e
raise e
Step 4: Handle the Webhook Endpoint
Combine the parser, mapper, and manager into a Flask or FastAPI endpoint. This example uses FastAPI for better async handling and type validation.
FastAPI Webhook Handler
from fastapi import FastAPI, HTTPException, Request
from typing import Dict, Any
import logging
app = FastAPI()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize dependencies
# In production, load these from environment variables
CXONE_AUTH = CxoneAuth(
env="us",
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET"
)
CXONE_MANAGER = CxoneConversationManager(CXONE_AUTH)
@app.post("/webhook/cognigy/routing")
async def handle_cognigy_webhook(payload: CognigyWebhookPayload):
"""
Receives intent data from Cognigy and routes the CXone conversation.
"""
conv_id = payload.get_conversation_id()
if not conv_id:
raise HTTPException(status_code=400, detail="Missing CXone Conversation ID in payload")
# Validate confidence
if not payload.is_confident(threshold=0.85):
logger.warning(f"Low confidence score {payload.intent.score} for intent {payload.intent.name}")
# Optionally return a different response to Cognigy to trigger fallback
return {"status": "low_confidence", "action": "fallback"}
# Determine target skill
target_skill = determine_target_skill(payload.intent.name)
try:
# Execute routing
result = CXONE_MANAGER.update_conversation_routing(
conversation_id=conv_id,
skill_group_name=target_skill.value,
intent_name=payload.intent.name,
intent_score=payload.intent.score
)
logger.info(f"Successfully routed conv {conv_id} to {target_skill.value}")
return {
"status": "success",
"routedTo": target_skill.value,
"cxoneResponse": result
}
except ValueError as ve:
raise HTTPException(status_code=404, detail=str(ve))
except RuntimeError as re:
raise HTTPException(status_code=409, detail=str(re))
except Exception as e:
logger.error(f"Unexpected error: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")
Complete Working Example
This is the full Python file (main.py) ready to deploy.
import os
import time
import requests
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field
from enum import Enum
from fastapi import FastAPI, HTTPException
import logging
# --- Configuration ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Pydantic Models ---
class CognigyEntity(BaseModel):
name: str
value: str
class CognigyIntent(BaseModel):
name: str
score: float
class CognigySession(BaseModel):
id: str
user: Dict[str, Any]
cxone: Dict[str, Any]
class CognigyWebhookPayload(BaseModel):
session: CognigySession
intent: CognigyIntent
entities: List[CognigyEntity] = []
text: str
def get_conversation_id(self) -> Optional[str]:
return self.session.cxone.get("conversationId")
def is_confident(self, threshold: float = 0.85) -> bool:
return self.intent.score >= threshold
# --- Routing Logic ---
class CxoneSkillGroup(str, Enum):
BILLING = "Billing Support"
TECHNICAL = "Technical Support"
SALES = "Sales Team"
DEFAULT = "General Inquiry"
INTENT_TO_SKILL_MAP = {
"billing_inquiry": CxoneSkillGroup.BILLING,
"payment_failure": CxoneSkillGroup.BILLING,
"login_issue": CxoneSkillGroup.TECHNICAL,
"feature_request": CxoneSkillGroup.SALES,
"product_bug": CxoneSkillGroup.TECHNICAL,
}
def determine_target_skill(intent_name: str) -> CxoneSkillGroup:
return INTENT_TO_SKILL_MAP.get(intent_name, CxoneSkillGroup.DEFAULT)
# --- CXone API Client ---
class CxoneClient:
def __init__(self, env: str, client_id: str, client_secret: str):
self.env = env
self.client_id = client_id
self.client_secret = client_secret
self.base_url = f"https://{env}.api.cxone.com" if env != "us" else "https://api.cxone.com"
self.token: Optional[str] = None
self.token_expiry: float = 0
def _get_token(self) -> str:
if self.token and time.time() < self.token_expiry:
return self.token
url = f"{self.base_url}/oauth/token"
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "conversation:all routing:all"
}
response = requests.post(url, data=data)
response.raise_for_status()
token_data = response.json()
self.token = token_data["access_token"]
self.token_expiry = time.time() + (token_data["expires_in"] - 30)
return self.token
def route_conversation(self, conv_id: str, skill_name: str, intent_data: Dict[str, Any]) -> Dict[str, Any]:
token = self._get_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# 1. Update Attributes
attr_payload = {
"attributes": {
"cognigyIntent": intent_data
}
}
try:
requests.patch(
f"{self.base_url}/api/v2/conversations/{conv_id}",
json=attr_payload,
headers=headers
)
except Exception as e:
logger.warning(f"Attribute update failed: {e}")
# 2. Route to Skill
routing_payload = {
"skillGroups": [{"name": skill_name}],
"routingMethod": "longestAvailable"
}
resp = requests.patch(
f"{self.base_url}/api/v2/conversations/{conv_id}/routing",
json=routing_payload,
headers=headers
)
resp.raise_for_status()
return resp.json()
# --- FastAPI App ---
app = FastAPI()
# Load secrets from environment
cxone_client = CxoneClient(
env=os.getenv("CXONE_ENV", "us"),
client_id=os.getenv("CXONE_CLIENT_ID"),
client_secret=os.getenv("CXONE_CLIENT_SECRET")
)
@app.post("/webhook/cognigy")
async def webhook_handler(payload: CognigyWebhookPayload):
conv_id = payload.get_conversation_id()
if not conv_id:
raise HTTPException(status_code=400, detail="No CXone Conversation ID found")
if not payload.is_confident():
return {"status": "fallback", "reason": "low_confidence"}
target_skill = determine_target_skill(payload.intent.name)
try:
result = cxone_client.route_conversation(
conv_id=conv_id,
skill_name=target_skill.value,
intent_data={
"name": payload.intent.name,
"score": payload.intent.score
}
)
return {"status": "routed", "skill": target_skill.value}
except requests.exceptions.HTTPError as e:
if e.response.status_code == 409:
raise HTTPException(status_code=409, detail="Conversation already routed")
raise HTTPException(status_code=500, detail=str(e))
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is expired or the Client ID/Secret is incorrect.
- Fix: Verify the credentials in the CXone Admin Console > Developers > API Access. Ensure the
CxoneClientclass correctly calculatestoken_expiry.
Error: 409 Conflict
- Cause: The conversation is already assigned to an agent or has ended.
- Fix: Check the conversation status in the CXone API response. If
stateisENDEDorACTIVEwith anagentassigned, do not attempt to route. Handle this in Cognigy by checking the webhook response status.
Error: 429 Too Many Requests
- Cause: You are hitting the CXone API rate limits (typically 100 requests per minute per client for some endpoints).
- Fix: Implement exponential backoff in the
CxoneClient._get_tokenandroute_conversationmethods.
import time
def make_api_call_with_retry(url, method, payload, max_retries=3):
for attempt in range(max_retries):
try:
resp = method(url, json=payload, headers=headers)
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 2 ** attempt))
time.sleep(retry_after)
continue
resp.raise_for_status()
return resp.json()
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1:
raise e
time.sleep(1)
Error: Intent Not Found in Map
- Cause: The Cognigy intent name does not match the keys in
INTENT_TO_SKILL_MAP. - Fix: Ensure the intent names in Cognigy Studio exactly match the keys in your Python dictionary. Case sensitivity matters.