Routing Cognigy Bot Conversations to NICE CXone Agents via Webhook Payloads

Routing Cognigy Bot Conversations to NICE CXone Agents via Webhook Payloads

What You Will Build

  • A Python service that receives a webhook payload from NICE Cognigy, extracts intent and entity data, and routes the conversation to a specific NICE CXone Queue or Agent based on dynamic business logic.
  • This tutorial utilizes the NICE CXone REST API (/api/v2/conversations/webchat and /api/v2/users/me) and the Python requests library.
  • The implementation is written in Python 3.9+ and assumes a standard Flask or FastAPI environment for hosting the webhook endpoint.

Prerequisites

  • OAuth Client Type: Service Account (Client Credentials Grant) or JWT Grant.
  • Required Scopes: conversations:webchat:create, conversations:webchat:read, users:read, queues:read.
  • SDK/API Version: NICE CXone REST API v2.
  • Language/Runtime: Python 3.9+.
  • External Dependencies: requests, flask, python-dotenv.

Install dependencies:

pip install requests flask python-dotenv

Authentication Setup

NICE CXone APIs require a valid Bearer token. For a backend webhook service, the Client Credentials Grant is the most robust method as it does not rely on user context. You must configure a Service Account in the NICE CXone Admin Console with the scopes listed above.

The following class handles token acquisition and caching. It avoids redundant API calls by caching the token until it expires.

import time
import requests
import os
from dotenv import load_dotenv

load_dotenv()

class CXoneAuth:
    def __init__(self):
        self.client_id = os.getenv("CXONE_CLIENT_ID")
        self.client_secret = os.getenv("CXONE_CLIENT_SECRET")
        self.region = os.getenv("CXONE_REGION", "mypurecloud.com")
        self.token_url = f"https://{self.region}/oauth/token"
        self.access_token = None
        self.token_expiry = 0

    def get_token(self) -> str:
        """
        Retrieves a valid OAuth2 access token.
        Caches the token to avoid unnecessary API calls.
        """
        # Check if token is still valid (subtract 60s for safety margin)
        if self.access_token and time.time() < self.token_expiry - 60:
            return self.access_token

        payload = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret
        }

        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }

        try:
            response = requests.post(self.token_url, data=payload, headers=headers)
            response.raise_for_status()
            data = response.json()
            
            self.access_token = data["access_token"]
            # Set expiry time based on issued_at + expires_in
            issued_at = time.time()
            self.token_expiry = issued_at + data["expires_in"]
            
            return self.access_token
        except requests.exceptions.HTTPError as e:
            raise Exception(f"Failed to authenticate with CXone: {e.response.text}")
        except Exception as e:
            raise Exception(f"Authentication error: {str(e)}")

# Initialize global auth instance
auth_client = CXoneAuth()

Implementation

Step 1: Parsing the Cognigy Webhook Payload

NICE Cognigy sends a POST request to your webhook URL when a specific event occurs (e.g., OnIntentMatch). The payload structure depends on your Cognigy design, but it typically contains the intent, entities, and session data.

We must parse this JSON to determine the routing strategy. For this tutorial, we assume the following routing logic:

  • Intent transfer_to_sales: Route to “Sales Queue”.
  • Intent transfer_to_support: Route to “Support Queue”.
  • Intent transfer_to_agent: Route to a specific Agent ID provided in an entity.
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook/cognigy', methods=['POST'])
def handle_cognigy_webhook():
    # 1. Validate Content-Type
    if not request.is_json:
        return jsonify({"error": "Content-Type must be application/json"}), 400

    data = request.json
    
    # 2. Extract Intent and Entities
    # Note: Adjust these keys based on your specific Cognigy output configuration
    intent_name = data.get("intent", {}).get("name")
    entities = data.get("entities", [])
    session_id = data.get("session", {}).get("sessionId")
    user_id = data.get("user", {}).get("id") # Cognigy User ID

    if not intent_name:
        return jsonify({"error": "Missing intent in payload"}), 400

    # 3. Determine Routing Target
    queue_id = None
    user_id_cxone = None

    if intent_name == "transfer_to_sales":
        queue_id = "SALES_QUEUE_ID_PLACEHOLDER" 
    elif intent_name == "transfer_to_support":
        queue_id = "SUPPORT_QUEUE_ID_PLACEHOLDER"
    elif intent_name == "transfer_to_agent":
        # Extract agent ID from entities
        agent_entity = next((e for e in entities if e.get("name") == "agentId"), None)
        if agent_entity:
            user_id_cxone = agent_entity.get("value")
        else:
            return jsonify({"error": "Agent ID entity missing for direct transfer"}), 400
    else:
        return jsonify({"error": "Unknown routing intent"}), 400

    # 4. Execute Routing
    result = route_to_cxone(session_id, user_id, queue_id, user_id_cxone)
    
    if result.get("success"):
        return jsonify({"status": "routed", "cxone_id": result.get("conversation_id")}), 200
    else:
        return jsonify({"error": result.get("message")}), 500

Step 2: Establishing the CXone Conversation

To route a conversation, you must first create a Webchat Conversation in CXone using the POST /api/v2/conversations/webchat endpoint. This creates the container for the interaction.

Important: The userId in the CXone API should be a unique identifier for the end-user. You can map the Cognigy user.id to this field.

def create_webchat_conversation(cognigy_user_id: str) -> str:
    """
    Creates a new Webchat conversation in CXone.
    Returns the Conversation ID.
    """
    token = auth_client.get_token()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    
    # Map Cognigy User ID to CXone User ID
    # Ensure this ID is unique within your CXone tenant
    cxone_user_id = f"cognigy_{cognigy_user_id}"
    
    payload = {
        "userId": cxone_user_id,
        "name": "Webchat from Cognigy",
        "labels": {
            "source": "cognigy"
        }
    }

    url = f"https://{auth_client.region}/api/v2/conversations/webchat"

    try:
        response = requests.post(url, json=payload, headers=headers)
        response.raise_for_status()
        data = response.json()
        return data["id"]
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 409:
            # User already has an active session
            return handle_existing_session(cxone_user_id)
        raise Exception(f"Failed to create conversation: {e.response.text}")

Step 3: Routing to Queue or Agent

Once the conversation exists, you must add it to a Queue or assign it to a specific User (Agent). This is done using the POST /api/v2/conversations/webchat/{conversationId}/actions endpoint.

OAuth Scope: conversations:webchat:write (often covered by conversations:webchat:create in some tenants, but explicit write scope is safer).

def route_conversation(conversation_id: str, queue_id: str = None, user_id_cxone: str = None) -> dict:
    """
    Routes an existing conversation to a Queue or Agent.
    """
    token = auth_client.get_token()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    url = f"https://{auth_client.region}/api/v2/conversations/webchat/{conversation_id}/actions"

    # Construct the action payload
    if queue_id:
        action_payload = {
            "action": "queue",
            "to": {
                "queueId": queue_id
            }
        }
    elif user_id_cxone:
        action_payload = {
            "action": "user",
            "to": {
                "userId": user_id_cxone
            }
        }
    else:
        return {"success": False, "message": "No target specified for routing"}

    try:
        response = requests.post(url, json=action_payload, headers=headers)
        
        # CXone often returns 204 No Content for successful actions
        if response.status_code == 204 or response.status_code == 200:
            return {"success": True, "conversation_id": conversation_id}
        else:
            response.raise_for_status()
    except requests.exceptions.HTTPError as e:
        error_msg = e.response.text
        if "409" in str(e):
            return {"success": False, "message": "Conversation already in desired state"}
        raise Exception(f"Routing failed: {error_msg}")

Step 4: Handling Existing Sessions and Context

If a user already has an open CXone session, creating a new one will fail with a 409 Conflict. You must retrieve the existing conversation ID. Additionally, you may want to push the Cognigy intent back into CXone as a label or attribute for reporting.

def handle_existing_session(cxone_user_id: str) -> str:
    """
    Retrieves the active conversation ID for a user.
    """
    token = auth_client.get_token()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    # Query active conversations for this user
    url = f"https://{auth_client.region}/api/v2/conversations/webchat"
    params = {
        "userId": cxone_user_id,
        "status": "ACTIVE"
    }

    try:
        response = requests.get(url, headers=headers, params=params)
        response.raise_for_status()
        data = response.json()
        
        if data["items"]:
            # Return the first active conversation
            return data["items"][0]["id"]
        else:
            # Fallback: Create new if no active session found despite 409
            # This can happen if the session expired but the lock remains briefly
            return create_webchat_conversation(cxone_user_id.split("_")[1])
    except Exception as e:
        raise Exception(f"Failed to retrieve existing session: {str(e)}")

def update_conversation_labels(conversation_id: str, intent: str) -> None:
    """
    Adds the Cognigy intent as a label to the CXone conversation for reporting.
    """
    token = auth_client.get_token()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    url = f"https://{auth_client.region}/api/v2/conversations/webchat/{conversation_id}/labels"
    
    # CXone labels are key-value pairs
    payload = {
        "labels": {
            "cognigy_intent": intent
        }
    }

    try:
        requests.put(url, json=payload, headers=headers)
    except Exception as e:
        # Log warning but do not fail the routing
        print(f"Warning: Failed to update labels: {str(e)}")

Step 5: Orchestrating the Flow

Combine the parsing, session management, and routing logic into the main handler.

def route_to_cxone(session_id: str, cognigy_user_id: str, queue_id: str = None, user_id_cxone: str = None) -> dict:
    """
    Main orchestration function.
    """
    try:
        # 1. Get or Create Conversation
        conversation_id = create_webchat_conversation(cognigy_user_id)
        
        # 2. Route to Target
        result = route_conversation(conversation_id, queue_id, user_id_cxone)
        
        if result.get("success"):
            # 3. Update Labels with Intent (Optional but recommended)
            # Note: You need to pass the intent name down from the webhook handler
            # For simplicity, we assume the intent is passed or retrieved from context
            # In a real app, pass 'intent_name' into this function
            pass 
            
            return result
        else:
            return result

    except Exception as e:
        return {"success": False, "message": str(e)}

# Update the Flask route to pass intent_name to the orchestrator
@app.route('/webhook/cognigy', methods=['POST'])
def handle_cognigy_webhook():
    if not request.is_json:
        return jsonify({"error": "Content-Type must be application/json"}), 400

    data = request.json
    intent_name = data.get("intent", {}).get("name")
    entities = data.get("entities", [])
    session_id = data.get("session", {}).get("sessionId")
    user_id = data.get("user", {}).get("id")

    if not intent_name:
        return jsonify({"error": "Missing intent in payload"}), 400

    queue_id = None
    user_id_cxone = None

    if intent_name == "transfer_to_sales":
        queue_id = "SALES_QUEUE_ID_PLACEHOLDER" 
    elif intent_name == "transfer_to_support":
        queue_id = "SUPPORT_QUEUE_ID_PLACEHOLDER"
    elif intent_name == "transfer_to_agent":
        agent_entity = next((e for e in entities if e.get("name") == "agentId"), None)
        if agent_entity:
            user_id_cxone = agent_entity.get("value")
        else:
            return jsonify({"error": "Agent ID entity missing"}), 400
    else:
        return jsonify({"error": "Unknown routing intent"}), 400

    result = route_to_cxone(session_id, user_id, queue_id, user_id_cxone)
    
    if result.get("success"):
        # Update labels here
        update_conversation_labels(result.get("conversation_id"), intent_name)
        return jsonify({"status": "routed", "cxone_id": result.get("conversation_id")}), 200
    else:
        return jsonify({"error": result.get("message")}), 500

Complete Working Example

Save this as cognigy_cxone_router.py. Ensure you have a .env file with CXONE_CLIENT_ID, CXONE_CLIENT_SECRET, and CXONE_REGION.

import os
import time
import requests
from flask import Flask, request, jsonify
from dotenv import load_dotenv

load_dotenv()

# --- Configuration ---
CXONE_REGION = os.getenv("CXONE_REGION", "mypurecloud.com")
CXONE_CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CXONE_CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")

# --- Auth Manager ---
class CXoneAuth:
    def __init__(self):
        self.token_url = f"https://{CXONE_REGION}/oauth/token"
        self.access_token = None
        self.token_expiry = 0

    def get_token(self) -> str:
        if self.access_token and time.time() < self.token_expiry - 60:
            return self.access_token

        payload = {
            "grant_type": "client_credentials",
            "client_id": CXONE_CLIENT_ID,
            "client_secret": CXONE_CLIENT_SECRET
        }

        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        try:
            response = requests.post(self.token_url, data=payload, headers=headers)
            response.raise_for_status()
            data = response.json()
            self.access_token = data["access_token"]
            self.token_expiry = time.time() + data["expires_in"]
            return self.access_token
        except requests.exceptions.HTTPError as e:
            raise Exception(f"Auth Failed: {e.response.text}")

auth_client = CXoneAuth()

# --- CXone API Helpers ---

def get_headers():
    return {
        "Authorization": f"Bearer {auth_client.get_token()}",
        "Content-Type": "application/json"
    }

def create_or_get_conversation(cognigy_user_id: str) -> str:
    cxone_user_id = f"cognigy_{cognigy_user_id}"
    url = f"https://{CXONE_REGION}/api/v2/conversations/webchat"
    
    # Try to create
    try:
        payload = {
            "userId": cxone_user_id,
            "name": "Webchat from Cognigy",
            "labels": {"source": "cognigy"}
        }
        response = requests.post(url, json=payload, headers=get_headers())
        if response.status_code == 201:
            return response.json()["id"]
        elif response.status_code == 409:
            # Already exists, fetch it
            return fetch_active_conversation(cxone_user_id)
        else:
            response.raise_for_status()
    except requests.exceptions.HTTPError as e:
        raise Exception(f"Conversation creation failed: {e.response.text}")

def fetch_active_conversation(cxone_user_id: str) -> str:
    url = f"https://{CXONE_REGION}/api/v2/conversations/webchat"
    params = {"userId": cxone_user_id, "status": "ACTIVE"}
    response = requests.get(url, headers=get_headers(), params=params)
    response.raise_for_status()
    data = response.json()
    if data["items"]:
        return data["items"][0]["id"]
    raise Exception("No active conversation found despite 409 conflict")

def route_conversation(conversation_id: str, queue_id: str = None, user_id: str = None) -> bool:
    url = f"https://{CXONE_REGION}/api/v2/conversations/webchat/{conversation_id}/actions"
    
    if queue_id:
        payload = {"action": "queue", "to": {"queueId": queue_id}}
    elif user_id:
        payload = {"action": "user", "to": {"userId": user_id}}
    else:
        return False

    response = requests.post(url, json=payload, headers=get_headers())
    return response.status_code in [200, 204]

def update_labels(conversation_id: str, intent: str) -> None:
    url = f"https://{CXONE_REGION}/api/v2/conversations/webchat/{conversation_id}/labels"
    payload = {"labels": {"cognigy_intent": intent}}
    try:
        requests.put(url, json=payload, headers=get_headers())
    except Exception as e:
        print(f"Label update failed: {e}")

# --- Flask App ---
app = Flask(__name__)

@app.route('/webhook/cognigy', methods=['POST'])
def webhook_handler():
    if not request.is_json:
        return jsonify({"error": "Invalid content type"}), 400

    data = request.json
    intent_name = data.get("intent", {}).get("name")
    entities = data.get("entities", [])
    user_id = data.get("user", {}).get("id")

    if not intent_name or not user_id:
        return jsonify({"error": "Missing required fields"}), 400

    queue_id = None
    agent_id = None

    # Routing Logic
    if intent_name == "transfer_to_sales":
        queue_id = "SALES_QUEUE_ID_PLACEHOLDER"
    elif intent_name == "transfer_to_support":
        queue_id = "SUPPORT_QUEUE_ID_PLACEHOLDER"
    elif intent_name == "transfer_to_agent":
        agent_entity = next((e for e in entities if e.get("name") == "agentId"), None)
        if agent_entity:
            agent_id = agent_entity.get("value")
        else:
            return jsonify({"error": "Agent ID missing"}), 400
    else:
        return jsonify({"error": "Unsupported intent"}), 400

    try:
        # 1. Get Conversation
        conv_id = create_or_get_conversation(user_id)
        
        # 2. Route
        success = route_conversation(conv_id, queue_id, agent_id)
        
        if success:
            update_labels(conv_id, intent_name)
            return jsonify({"status": "success", "conversationId": conv_id}), 200
        else:
            return jsonify({"error": "Routing failed"}), 500

    except Exception as e:
        return jsonify({"error": str(e)}), 500

if __name__ == '__main__':
    app.run(port=5000, debug=True)

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token is expired, invalid, or the Client ID/Secret is incorrect.
  • Fix: Verify the credentials in your .env file. Ensure the Service Account is active in CXone Admin. Check the CXoneAuth.get_token() method for errors.

Error: 403 Forbidden

  • Cause: The Service Account lacks the required scopes (conversations:webchat:create, conversations:webchat:write).
  • Fix: Navigate to CXone Admin > Security > Applications. Edit your Service Account and add the missing scopes. Re-authorize the application if necessary.

Error: 409 Conflict

  • Cause: A conversation with the same userId already exists and is active.
  • Fix: This is expected behavior. The code handles this by querying for the existing active conversation. If the query fails, the user may have been disconnected. Ensure your fetch_active_conversation logic is robust.

Error: 429 Too Many Requests

  • Cause: You have exceeded the rate limit for CXone API calls.
  • Fix: Implement exponential backoff in your requests calls. For high-volume bots, cache tokens and batch label updates if possible.

Error: Invalid Queue ID

  • Cause: The queue_id placeholder is not replaced with a valid UUID from your CXone tenant.
  • Fix: Replace SALES_QUEUE_ID_PLACEHOLDER with the actual Queue ID found in CXone Admin > Queues.

Official References