Trigger NICE CXone Outbound Calls with the Personal Connection API
What You Will Build
- You will build a script that initiates an outbound call from a CXone agent to a specific destination number.
- You will use the NICE CXone Personal Connection API endpoint to execute the call trigger.
- You will use Python with the
requestslibrary to handle authentication and the HTTP POST request.
Prerequisites
- OAuth Client Type: A CXone OAuth Client configured with Client Credentials grant type.
- Required Scopes: The client must have the
calls:outboundscope. If you are triggering calls on behalf of a specific agent, you may also needagents:readto verify agent status. - SDK/API Version: CXone REST API v2.
- Language/Runtime: Python 3.8+.
- External Dependencies:
requestslibrary (pip install requests).
Authentication Setup
CXone uses OAuth 2.0 for API authentication. For automated scripts, the Client Credentials flow is the standard approach. This flow requires a client_id and client_secret obtained from the CXone Developer Portal.
The token endpoint is always https://<your-subdomain>.cxone.com/oauth2/token.
Token Retrieval Logic
You must retrieve a fresh access token before making API calls. Tokens typically expire in 3600 seconds. Your application should cache the token and check expiration before requesting a new one.
import requests
import time
import json
class CxoneAuth:
def __init__(self, subdomain: str, client_id: str, client_secret: str):
self.subdomain = subdomain
self.client_id = client_id
self.client_secret = client_secret
self.token_endpoint = f"https://{subdomain}.cxone.com/oauth2/token"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
"""
Retrieves an OAuth access token using Client Credentials.
Returns the token string. Raises an exception if authentication fails.
"""
# Check if we have a valid token cached
if self.access_token and time.time() < self.token_expiry:
return self.access_token
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
# The body for Client Credentials grant
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(self.token_endpoint, headers=headers, data=data)
response.raise_for_status() # Raises HTTPError for 4xx/5xx responses
except requests.exceptions.HTTPError as err:
raise Exception(f"OAuth Authentication Failed: {err}") from err
except requests.exceptions.RequestException as err:
raise Exception(f"Network error during OAuth request: {err}") from err
response_json = response.json()
if "access_token" not in response_json:
raise Exception("OAuth response did not contain an access_token")
self.access_token = response_json["access_token"]
self.token_expiry = time.time() + response_json.get("expires_in", 3600) - 10 # Subtract 10s buffer
return self.access_token
Implementation
Step 1: Construct the Personal Connection Request
The Personal Connection API allows an agent (or an automated process acting as an agent) to place a call. The endpoint is:
POST https://{subdomain}.cxone.com/api/v2/callcenter/outbound/personalconnections
OAuth Scope Required: calls:outbound
The request body must contain the destination object. The most critical fields are:
contact: The phone number to call.channel: Usuallyvoicefor outbound calls.callType: TypicallyOUTBOUND.
You must also identify the agent placing the call. This is done via the agent object, which requires the agent’s id and usually their loginId or externalId depending on your CXone configuration. For Personal Connection, the agent field in the request body often refers to the agent who is making the call.
Request Payload Structure
{
"destination": {
"contact": "+15550199888",
"channel": "voice",
"callType": "OUTBOUND"
},
"agent": {
"id": "5f8d9c7b-1234-5678-9abc-def012345678",
"loginId": "agent.john.doe@company.com"
},
"metadata": {
"subject": "Customer Follow-up",
"description": "Checking on recent order status."
}
}
Step 2: Execute the Outbound Call
This step combines the authentication logic with the actual API call. We will create a function that accepts the agent details and the destination number.
Critical Note on Agent Status: The agent initiating the personal connection must be in a state that allows outbound calls. Typically, this means the agent must be Logged In and Available or in a specific Outbound state. If the agent is busy or offline, the API will return a 400 Bad Request or 403 Forbidden.
import requests
from typing import Dict, Any
class CxonePersonalConnection:
def __init__(self, subdomain: str, auth_manager: CxoneAuth):
self.subdomain = subdomain
self.auth_manager = auth_manager
self.base_url = f"https://{subdomain}.cxone.com/api/v2/callcenter/outbound/personalconnections"
def trigger_outbound_call(self, agent_id: str, agent_login_id: str, destination_number: str, subject: str = "") -> Dict[str, Any]:
"""
Triggers an outbound call for a specific agent using Personal Connection.
Args:
agent_id: The UUID of the agent placing the call.
agent_login_id: The login ID (email) of the agent.
destination_number: The E.164 formatted phone number to call.
subject: Optional subject line for the call record.
Returns:
The JSON response from the CXone API.
"""
token = self.auth_manager.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = {
"destination": {
"contact": destination_number,
"channel": "voice",
"callType": "OUTBOUND"
},
"agent": {
"id": agent_id,
"loginId": agent_login_id
}
}
if subject:
payload["metadata"] = {
"subject": subject
}
try:
response = requests.post(self.base_url, headers=headers, json=payload)
# Handle specific HTTP errors
if response.status_code == 401:
raise Exception("Authentication failed. Token may be expired or invalid.")
elif response.status_code == 403:
raise Exception("Forbidden. Check OAuth scopes (calls:outbound) and agent permissions.")
elif response.status_code == 400:
error_body = response.json()
raise Exception(f"Bad Request: {error_body.get('reason', 'Unknown error')} - {error_body.get('description', '')}")
elif response.status_code == 429:
# Implement retry logic in production
raise Exception("Rate Limited (429). Please wait and retry.")
response.raise_for_status() # Raise for other 4xx/5xx
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"Network error while triggering call: {str(e)}") from e
Step 3: Processing Results and Validation
The API response for a successful Personal Connection trigger is minimal. It typically returns a 200 OK or 201 Created. The response body might contain a callId or simply confirm the action.
However, the most important validation is checking if the call was actually initiated. The CXone platform may return a success code even if the agent’s client fails to pick up the call instruction due to local client issues.
To verify the call, you should query the Call Details API using the callId if provided, or monitor the agent’s call log.
def verify_call_status(subdomain: str, auth_manager: CxoneAuth, call_id: str) -> Dict[str, Any]:
"""
Optional: Verify if the call was successfully created by querying call details.
Note: Personal Connection responses do not always return a callId immediately.
This is a secondary validation step.
"""
if not call_id:
return {"status": "unknown", "message": "No call ID provided to verify."}
token = auth_manager.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json"
}
# Endpoint for retrieving call details
url = f"https://{subdomain}.cxone.com/api/v2/analytics/conversations/details/query"
# Construct a query for the specific conversation ID
query_body = {
"query": {
"filter": [
{
"field": "conversationId",
"operator": "equals",
"value": call_id
}
]
},
"interval": {
"from": "2023-01-01T00:00:00.000Z", # Adjust as needed
"to": "2023-12-31T23:59:59.999Z"
}
}
try:
response = requests.post(url, headers=headers, json=query_body)
response.raise_for_status()
return response.json()
except Exception as e:
return {"status": "error", "message": str(e)}
Complete Working Example
This is a full, copy-pasteable script. Replace the placeholder values with your actual CXone credentials.
import requests
import time
import json
import sys
from typing import Dict, Any
# --- Configuration ---
# Replace these with your actual CXone credentials
CXONE_SUBDOMAIN = "your-subdomain" # e.g., "mycompany"
OAUTH_CLIENT_ID = "your_client_id"
OAUTH_CLIENT_SECRET = "your_client_secret"
# Agent details for the outbound call
AGENT_ID = "5f8d9c7b-1234-5678-9abc-def012345678" # Replace with real Agent UUID
AGENT_LOGIN_ID = "agent.name@company.com" # Replace with real Agent Login ID
DESTINATION_NUMBER = "+15550199888" # E.164 format
# --- Authentication Class ---
class CxoneAuth:
def __init__(self, subdomain: str, client_id: str, client_secret: str):
self.subdomain = subdomain
self.client_id = client_id
self.client_secret = client_secret
self.token_endpoint = f"https://{subdomain}.cxone.com/oauth2/token"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry:
return self.access_token
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(self.token_endpoint, headers=headers, data=data)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise Exception(f"OAuth Authentication Failed: {err}") from err
except requests.exceptions.RequestException as err:
raise Exception(f"Network error during OAuth request: {err}") from err
response_json = response.json()
if "access_token" not in response_json:
raise Exception("OAuth response did not contain an access_token")
self.access_token = response_json["access_token"]
self.token_expiry = time.time() + response_json.get("expires_in", 3600) - 10
return self.access_token
# --- Personal Connection Logic ---
class CxonePersonalConnection:
def __init__(self, subdomain: str, auth_manager: CxoneAuth):
self.subdomain = subdomain
self.auth_manager = auth_manager
self.base_url = f"https://{subdomain}.cxone.com/api/v2/callcenter/outbound/personalconnections"
def trigger_outbound_call(self, agent_id: str, agent_login_id: str, destination_number: str, subject: str = "") -> Dict[str, Any]:
token = self.auth_manager.get_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = {
"destination": {
"contact": destination_number,
"channel": "voice",
"callType": "OUTBOUND"
},
"agent": {
"id": agent_id,
"loginId": agent_login_id
}
}
if subject:
payload["metadata"] = {"subject": subject}
try:
response = requests.post(self.base_url, headers=headers, json=payload)
if response.status_code == 401:
print("Error: Authentication failed. Check Client ID/Secret and Token.")
sys.exit(1)
elif response.status_code == 403:
print("Error: Forbidden. Ensure OAuth Client has 'calls:outbound' scope and Agent has permissions.")
sys.exit(1)
elif response.status_code == 400:
error_body = response.json()
print(f"Error: Bad Request - {error_body.get('reason', 'Unknown')}")
print(f"Details: {error_body.get('description', '')}")
sys.exit(1)
elif response.status_code == 429:
print("Error: Rate Limited. Please wait before retrying.")
sys.exit(1)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Network error: {str(e)}")
raise
# --- Main Execution ---
def main():
print(f"Initializing CXone Client for subdomain: {CXONE_SUBDOMAIN}")
auth_manager = CxoneAuth(CXONE_SUBDOMAIN, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET)
pc_client = CxonePersonalConnection(CXONE_SUBDOMAIN, auth_manager)
print(f"Triggering outbound call for Agent: {AGENT_LOGIN_ID} to {DESTINATION_NUMBER}")
try:
result = pc_client.trigger_outbound_call(
agent_id=AGENT_ID,
agent_login_id=AGENT_LOGIN_ID,
destination_number=DESTINATION_NUMBER,
subject="API Triggered Call"
)
print("Call triggered successfully!")
print(f"Response: {json.dumps(result, indent=2)}")
except Exception as e:
print(f"Failed to trigger call: {str(e)}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is expired, invalid, or the Client ID/Secret is incorrect.
- Fix: Ensure your
CxoneAuthclass is correctly fetching a new token. Check that the Client ID and Secret match the credentials created in the CXone Developer Portal. Verify the token endpoint URL uses your correct subdomain.
Error: 403 Forbidden
- Cause: The OAuth Client does not have the required
calls:outboundscope, or the Agent does not have permission to make outbound calls. - Fix:
- Go to the CXone Developer Portal.
- Edit your OAuth Client.
- Ensure the Scopes tab includes
calls:outbound. - Verify the Agent is assigned to a Role that permits outbound calling.
Error: 400 Bad Request - “Agent is not available”
- Cause: The agent specified in the request is not logged into the CXone Agent Desktop, or is in a state that does not allow outbound calls (e.g., Offline, Break, or on another call).
- Fix:
- Log in to the CXone Agent Desktop as the target agent.
- Ensure the agent is in the Available state.
- If using a specific Outbound Campaign, ensure the agent is assigned to that campaign.
- Check the
descriptionfield in the 400 response for specific details about the agent’s state.
Error: 400 Bad Request - “Invalid Contact”
- Cause: The phone number provided in
destination.contactis not in valid E.164 format. - Fix: Ensure the number starts with a
+followed by the country code and the number (e.g.,+15551234567). Do not include dashes, spaces, or parentheses.
Error: 429 Too Many Requests
- Cause: You have exceeded the rate limit for the Personal Connection endpoint.
- Fix: Implement exponential backoff in your retry logic. The CXone API returns
Retry-Afterheaders in some cases, but generally, spacing requests by 1-2 seconds is recommended for bulk operations.