Routing Cognigy Intents to NICE CXone Queues via Webhook and API
What You Will Build
- You will build a Python middleware service that receives a webhook from NICE Cognigy, interprets the detected intent, and routes the conversation to the appropriate NICE CXone queue using the Engage API.
- This solution uses the NICE CXone REST API (
/api/v2/engage/conversations) and the Pythonrequestslibrary. - The tutorial covers Python for the middleware and JSON for the webhook payload definition.
Prerequisites
- NICE CXone OAuth Client: A Service Account or Client Credentials setup with the following scopes:
conversation:readconversation:writerouting:queue:readrouting:skill:read
- NICE Cognigy Project: A configured flow with an Outgoing Webhook action targeting your middleware endpoint.
- Python Environment: Python 3.8+ with
requestsandpydanticinstalled. - Dependencies:
pip install requests pydantic python-dotenv
Authentication Setup
NICE CXone APIs require a valid OAuth 2.0 Bearer token. Since this is a server-to-server integration (Cognigy Middleware to CXone), the Client Credentials flow is the standard approach. You must implement token caching to avoid hitting the identity provider on every webhook hit.
Step 1: Implement Token Management
Create a helper class to handle authentication. This class fetches a token from the CXone Identity endpoint and caches it until expiration.
import requests
import time
import os
from typing import Optional
class CXoneAuth:
def __init__(self, client_id: str, client_secret: str, org_id: str):
self.client_id = client_id
self.client_secret = client_secret
self.org_id = org_id
self.token_url = f"https://{org_id}.api.cXone.com/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0
def get_token(self) -> str:
"""
Returns a valid access token. Fetches a new one if the current one is missing or expired.
"""
current_time = time.time()
# Check if we have a valid token cached
if self.access_token and current_time < self.token_expiry:
return self.access_token
# Fetch new 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"]
# Expire the token slightly before actual expiry to prevent race conditions
self.token_expiry = current_time + data["expires_in"] - 10
return self.access_token
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("Invalid Client ID or Secret") from e
elif response.status_code == 429:
# Simple retry logic for rate limiting
time.sleep(2)
return self.get_token()
else:
raise Exception(f"Authentication failed: {response.text}") from e
# Usage Example
# auth = CXoneAuth(
# client_id=os.getenv("CXONE_CLIENT_ID"),
# client_secret=os.getenv("CXONE_CLIENT_SECRET"),
# org_id=os.getenv("CXONE_ORG_ID")
# )
# token = auth.get_token()
Step 2: Define the Webhook Payload Structure
In NICE Cognigy, you must configure the Outgoing Webhook to send a specific JSON structure. You need to map Cognigy’s intent.name to a CXone Queue ID.
Configure your Cognigy Outgoing Webhook to send the following JSON structure. Ensure you replace {{intent.name}} and {{sessionId}} with actual Cognigy expression variables in the Cognigy Studio configuration.
{
"sessionId": "{{sessionId}}",
"intent": "{{intent.name}}",
"confidence": {{intent.confidence}},
"userEmail": "{{user.email}}",
"userName": "{{user.name}}",
"channel": "webchat"
}
Step 3: Map Intents to CXone Queue IDs
You need a mapping layer in your Python code. Hardcoding Queue IDs is fragile. Instead, fetch the Queue ID by name during initialization or use a static map if your queue structure is stable. For this tutorial, we will use a static map for simplicity, but in production, you should cache the queue lookup from /api/v2/routing/queues.
# Mapping Cognigy Intent Names to CXone Queue IDs
# These IDs must be retrieved from your CXone instance via the Routing API
INTENT_TO_QUEUE_MAP = {
"sales_inquiry": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"billing_question": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"technical_support": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"default": "d4e5f6a7-b8c9-0123-defa-234567890123"
}
Implementation
Step 4: Build the Middleware Endpoint
You will create a Flask or FastAPI endpoint to receive the webhook. Here we use Flask for its simplicity in handling raw JSON payloads.
from flask import Flask, request, jsonify
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize Auth (load env vars in real app)
# auth = CXoneAuth(...)
@app.route('/webhook/cognigy', methods=['POST'])
def handle_cognigy_webhook():
"""
Receives intent data from Cognigy and routes to CXone.
"""
try:
data = request.get_json()
if not data:
return jsonify({"error": "No JSON payload provided"}), 400
intent_name = data.get("intent")
session_id = data.get("sessionId")
if not intent_name or not session_id:
return jsonify({"error": "Missing intent or sessionId"}), 400
logger.info(f"Received intent: {intent_name} for session: {session_id}")
# Determine the target queue
queue_id = INTENT_TO_QUEUE_MAP.get(intent_name, INTENT_TO_QUEUE_MAP["default"])
# Route the conversation
result = route_to_cxone_queue(queue_id, session_id, data)
return jsonify({"status": "success", "cxone_action": result}), 200
except Exception as e:
logger.error(f"Webhook processing failed: {str(e)}")
return jsonify({"error": "Internal Server Error"}), 500
def route_to_cxone_queue(queue_id: str, cognigy_session_id: str, payload: dict) -> dict:
"""
Calls the CXone Engage API to route the conversation.
"""
# In a real scenario, you need to link this to an existing CXone Conversation ID
# or create a new one. This example assumes we are updating an existing
# conversation or creating a new engagement.
# NOTE: The actual routing logic depends on whether you are:
# 1. Creating a new conversation from scratch (if Cognigy is the entry point)
# 2. Updating an existing CXone conversation (if Cognigy is mid-flow)
# For this tutorial, we assume Cognigy is the entry point and we are
# creating a new conversation that is immediately routed.
org_id = os.getenv("CXONE_ORG_ID")
base_url = f"https://{org_id}.api.cXone.com"
endpoint = f"{base_url}/api/v2/engage/conversations"
token = auth.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
# Construct the CXone Conversation Payload
# This payload creates a new conversation and assigns it to a queue
cxone_payload = {
"type": "webchat",
"initialQueueId": queue_id,
"externalContact": {
"id": cognigy_session_id, # Use Cognigy Session ID as external reference
"name": payload.get("userName", "Anonymous"),
"email": payload.get("userEmail", "unknown@example.com")
},
"initialQueueId": queue_id,
"attributes": {
"cognigy_intent": payload.get("intent"),
"cognigy_confidence": payload.get("confidence"),
"source": "cognigy_webhook"
}
}
try:
response = requests.post(endpoint, json=cxone_payload, headers=headers)
response.raise_for_status()
result = response.json()
logger.info(f"Successfully routed to CXone. Conversation ID: {result.get('id')}")
return result
except requests.exceptions.HTTPError as e:
logger.error(f"CXone API Error: {response.status_code} - {response.text}")
raise e
if __name__ == '__main__':
app.run(port=5000)
Step 5: Handle Complex Routing with Skills
If your routing relies on Skills rather than just Queues, you must include the requiredSkills array in the payload. The CXone API allows you to specify both an initialQueueId and requiredSkills.
Modify the cxone_payload in the previous step if you need skill-based routing:
cxone_payload = {
"type": "webchat",
"initialQueueId": queue_id,
"requiredSkills": [
{
"id": "skill_id_from_cxone_api",
"level": "basic" # or "expert", "advanced"
}
],
"externalContact": {
"id": cognigy_session_id,
"name": payload.get("userName", "Anonymous"),
"email": payload.get("userEmail", "unknown@example.com")
},
"attributes": {
"cognigy_intent": payload.get("intent")
}
}
Step 6: Verify Queue Availability
Before routing, it is best practice to check if the queue is open and has available agents. You can do this by calling the /api/v2/routing/queues/{queueId} endpoint.
def check_queue_status(auth: CXoneAuth, queue_id: str) -> bool:
"""
Checks if a CXone queue is open and accepting interactions.
"""
org_id = os.getenv("CXONE_ORG_ID")
endpoint = f"https://{org_id}.api.cXone.com/api/v2/routing/queues/{queue_id}"
token = auth.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json"
}
try:
response = requests.get(endpoint, headers=headers)
response.raise_for_status()
queue_data = response.json()
# Check if the queue is open
is_open = queue_data.get("open")
# Check if there are available agents (optional, depends on strategy)
# available_agents = queue_data.get("statistics", {}).get("availableAgents", 0)
return is_open if is_open is not None else True
except requests.exceptions.RequestException as e:
logger.warning(f"Failed to check queue status for {queue_id}: {e}")
return True # Fail open or close based on business logic
Integrate this check into your route_to_cxone_queue function. If the queue is closed, you can fall back to a default queue or a voicemail strategy.
Complete Working Example
Here is the complete, consolidated Python script. Save this as cognigy_cxone_router.py.
import os
import time
import requests
import logging
from flask import Flask, request, jsonify
from typing import Optional
# Configure Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# --- Configuration ---
INTENT_TO_QUEUE_MAP = {
"sales_inquiry": "YOUR_SALES_QUEUE_ID",
"billing_question": "YOUR_BILLING_QUEUE_ID",
"technical_support": "YOUR_TECH_QUEUE_ID",
"default": "YOUR_DEFAULT_QUEUE_ID"
}
class CXoneAuth:
def __init__(self, client_id: str, client_secret: str, org_id: str):
self.client_id = client_id
self.client_secret = client_secret
self.org_id = org_id
self.token_url = f"https://{org_id}.api.cXone.com/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0
def get_token(self) -> str:
current_time = time.time()
if self.access_token and current_time < self.token_expiry:
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"]
self.token_expiry = current_time + data["expires_in"] - 10
return self.access_token
except requests.exceptions.HTTPError as e:
raise Exception(f"Auth failed: {e}") from e
# Initialize App
app = Flask(__name__)
# Load Environment Variables
CXONE_CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CXONE_CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
CXONE_ORG_ID = os.getenv("CXONE_ORG_ID")
if not all([CXONE_CLIENT_ID, CXONE_CLIENT_SECRET, CXONE_ORG_ID]):
raise Exception("Missing environment variables: CXONE_CLIENT_ID, CXONE_CLIENT_SECRET, CXONE_ORG_ID")
auth = CXoneAuth(CXONE_CLIENT_ID, CXONE_CLIENT_SECRET, CXONE_ORG_ID)
@app.route('/webhook/cognigy', methods=['POST'])
def handle_cognigy_webhook():
try:
data = request.get_json()
if not data:
return jsonify({"error": "No JSON payload"}), 400
intent_name = data.get("intent")
session_id = data.get("sessionId")
if not intent_name or not session_id:
return jsonify({"error": "Missing intent or sessionId"}), 400
# Map Intent to Queue
queue_id = INTENT_TO_QUEUE_MAP.get(intent_name, INTENT_TO_QUEUE_MAP["default"])
# Route
result = route_to_cxone(queue_id, session_id, data)
return jsonify({"status": "success", "cxone_response": result}), 200
except Exception as e:
logger.error(f"Error: {str(e)}")
return jsonify({"error": "Processing failed"}), 500
def route_to_cxone(queue_id: str, session_id: str, payload: dict) -> dict:
org_id = CXONE_ORG_ID
endpoint = f"https://{org_id}.api.cXone.com/api/v2/engage/conversations"
token = auth.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
cxone_payload = {
"type": "webchat",
"initialQueueId": queue_id,
"externalContact": {
"id": session_id,
"name": payload.get("userName", "Guest"),
"email": payload.get("userEmail", "guest@example.com")
},
"attributes": {
"cognigy_intent": payload.get("intent"),
"cognigy_confidence": payload.get("confidence")
}
}
response = requests.post(endpoint, json=cxone_payload, headers=headers)
response.raise_for_status()
return response.json()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is expired, invalid, or the Client ID/Secret is incorrect.
- Fix: Verify your
CXoneAuthclass is fetching a fresh token. Check that the Client Credentials have theconversation:writescope. - Code Check: Ensure
response.raise_for_status()in the auth block catches 401s.
Error: 403 Forbidden
- Cause: The OAuth client lacks the required scopes (
conversation:write,routing:queue:read). - Fix: Go to the CXone Admin Console → Security → OAuth → Clients. Edit your client and add the missing scopes.
Error: 400 Bad Request
- Cause: The JSON payload sent to CXone is malformed or missing required fields.
- Fix: Ensure
initialQueueIdis a valid UUID. EnsureexternalContact.idis present. Use theloggingmodule to print the exact JSON sent to CXone.
Error: 429 Too Many Requests
- Cause: You are hitting the CXone API rate limits.
- Fix: Implement exponential backoff in your
requestscalls. TheCXoneAuthclass above includes a simple retry for 429s during token fetch. Apply similar logic to theroute_to_cxonefunction.