Building Dynamic Intent-Based Routing from Cognigy to NICE CXone

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/conversations and /api/v2/routing/users endpoints.
  • 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, and pydantic installed.
  • 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:

  1. Update the conversation’s custom attributes with the intent.
  2. Use the routing endpoint to assign the conversation to a specific skillGroup or userGroup.

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 CxoneClient class correctly calculates token_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 state is ENDED or ACTIVE with an agent assigned, 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_token and route_conversation methods.
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.

Official References