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/webchatand/api/v2/users/me) and the Pythonrequestslibrary. - 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
.envfile. Ensure the Service Account is active in CXone Admin. Check theCXoneAuth.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
userIdalready 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_conversationlogic is robust.
Error: 429 Too Many Requests
- Cause: You have exceeded the rate limit for CXone API calls.
- Fix: Implement exponential backoff in your
requestscalls. For high-volume bots, cache tokens and batch label updates if possible.
Error: Invalid Queue ID
- Cause: The
queue_idplaceholder is not replaced with a valid UUID from your CXone tenant. - Fix: Replace
SALES_QUEUE_ID_PLACEHOLDERwith the actual Queue ID found in CXone Admin > Queues.