Triggering a CXone Outbound Call via Personal Connection API
What You Will Build
- This tutorial demonstrates how to programmatically initiate an outbound voice call using the NICE CXone Personal Connection API.
- The solution utilizes the CXone REST API to create a
personalConnectionresource, which instructs the platform to dial a specified number. - The implementation is provided in Python using the
requestslibrary for precise control over HTTP headers and payload construction.
Prerequisites
- OAuth Client: You must have a CXone OAuth Client ID and Client Secret with the
offline_accessscope to generate refresh tokens. - Required Scopes: The access token must include the
personal-connection:writescope. Without this, the API will return a 403 Forbidden error. - CXone Environment: Ensure your CXone instance has the Personal Connection feature enabled. This is typically available in standard CXone deployments but may require specific licensing or feature flags depending on your contract.
- Python Runtime: Python 3.8 or higher.
- Dependencies:
requests: For HTTP communication.python-dotenv: For secure management of environment variables.
Install the dependencies via pip:
pip install requests python-dotenv
Authentication Setup
CXone uses OAuth 2.0 for authentication. The most robust method for server-to-server applications is the Client Credentials Grant. This flow provides an access token that is valid for a limited duration (typically 1 hour). For production applications, you should implement token caching and refresh logic. For this tutorial, we will fetch a fresh token on every execution to ensure simplicity and reliability in the example.
The endpoint for token generation is https://api.nicecxone.com/oauth/token.
import requests
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
CXONE_CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CXONE_CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
CXONE_BASE_URL = "https://api.nicecxone.com"
def get_access_token() -> str:
"""
Retrieves an OAuth 2.0 access token from CXone using Client Credentials Grant.
Returns:
str: The JWT access token.
Raises:
requests.exceptions.HTTPError: If the token request fails.
"""
token_url = f"{CXONE_BASE_URL}/oauth/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"client_id": CXONE_CLIENT_ID,
"client_secret": CXONE_CLIENT_SECRET,
"scope": "personal-connection:write"
}
response = requests.post(token_url, headers=headers, data=data)
# Raise an exception for bad status codes (4xx, 5xx)
response.raise_for_status()
token_json = response.json()
if "access_token" not in token_json:
raise ValueError("Failed to retrieve access_token from response")
return token_json["access_token"]
Security Note: Never hardcode CXONE_CLIENT_ID or CXONE_CLIENT_SECRET in your source code. Use environment variables or a secrets manager. The requests.post method with data=data automatically URL-encodes the form parameters, which is required by the OAuth endpoint.
Implementation
Step 1: Constructing the Personal Connection Payload
The core of the Personal Connection API is the POST /v1/personal-connections endpoint. This endpoint expects a JSON payload that defines the type of connection (voice, SMS, etc.), the target, and any associated metadata.
For a voice call, the type field must be set to voice. The target object contains the phone number to be dialed. It is critical that the phone number is formatted in E.164 format (e.g., +14155552671). If the number is not in E.164 format, the CXone platform may fail to route the call or return a validation error.
The payload also supports a context object. This is useful for passing data that might be used by IVR systems or for logging purposes. While not strictly required for the call to connect, it is best practice to include a unique identifier for tracing.
def build_personal_connection_payload(
target_number: str,
caller_id: str | None = None,
context_data: dict | None = None
) -> dict:
"""
Constructs the JSON payload for a CXone Personal Connection voice call.
Args:
target_number (str): The phone number to dial in E.164 format.
caller_id (str, optional): The outbound caller ID to use. Must be a verified number in your CXone instance.
context_data (dict, optional): Additional metadata to pass with the call.
Returns:
dict: The JSON-serializable payload.
"""
payload = {
"type": "voice",
"target": {
"phoneNumber": target_number
}
}
# Optional: Specify a caller ID if you have verified outbound numbers
if caller_id:
payload["callerId"] = caller_id
# Optional: Add context for IVR or logging
if context_data:
payload["context"] = context_data
return payload
Key Parameter Explanation:
type: Must be"voice"for outbound calls. Other values include"sms"and"email".target.phoneNumber: The destination number. Must be E.164 compliant.callerId: The number that appears on the recipient’s phone. This number must be registered and verified in your CXone instance under “Outbound Caller IDs”. If omitted, CXone uses the default fallback caller ID configured in your organization settings.context: A flexible JSON object. You can use this to pass campaign IDs, customer IDs, or other business logic data. This data is available in CXone Studio or via the Interaction APIs if the call is answered and routed to an agent.
Step 2: Executing the API Call
With the token and payload ready, we can make the POST request. The Personal Connection API is asynchronous. When you submit the request, CXone returns a personalConnectionId. This ID is crucial for tracking the status of the call (e.g., ringing, answered, failed).
We will use the requests library to send the POST request. We must include the Authorization: Bearer <token> header and set the Content-Type to application/json.
def trigger_outbound_call(
access_token: str,
target_number: str,
caller_id: str | None = None,
context_data: dict | None = None
) -> dict:
"""
Triggers an outbound voice call via the CXone Personal Connection API.
Args:
access_token (str): Valid OAuth 2.0 access token.
target_number (str): Destination phone number in E.164 format.
caller_id (str, optional): Verified outbound caller ID.
context_data (dict, optional): Metadata for the call.
Returns:
dict: The response from CXone containing the personalConnectionId.
Raises:
requests.exceptions.HTTPError: If the API call fails.
"""
api_endpoint = f"{CXONE_BASE_URL}/v1/personal-connections"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = build_personal_connection_payload(
target_number=target_number,
caller_id=caller_id,
context_data=context_data
)
print(f"Triggering call to {target_number}...")
print(f"Payload: {payload}")
response = requests.post(api_endpoint, headers=headers, json=payload)
# Handle HTTP errors
if response.status_code == 401:
raise Exception("Authentication failed. Check your OAuth token.")
elif response.status_code == 403:
raise Exception("Forbidden. Ensure your client has the 'personal-connection:write' scope.")
elif response.status_code == 400:
error_detail = response.json().get("message", "Unknown error")
raise ValueError(f"Bad Request: {error_detail}")
elif response.status_code == 429:
raise Exception("Rate limit exceeded. Please retry after the Retry-After header duration.")
else:
response.raise_for_status()
return response.json()
Step 3: Handling the Response and Tracking
The response from the POST /v1/personal-connections endpoint is not the result of the call (i.e., it does not tell you if the person answered). It is an acknowledgment that the request was accepted.
A successful response (HTTP 201 Created) looks like this:
{
"personalConnectionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "voice",
"status": "pending",
"createdTime": "2023-10-27T10:00:00.000Z",
"target": {
"phoneNumber": "+14155552671"
}
}
Important: The status field in the immediate response is almost always "pending" or "initiated". To know if the call was answered, you must either:
- Use CXone Studio to handle the interaction flow and log outcomes.
- Poll the CXone Interaction API using the
personalConnectionIdas a reference (if linked). - Subscribe to CXone Webhooks for real-time status updates.
For this tutorial, we will focus on the initiation. However, it is good practice to log the personalConnectionId for debugging.
Complete Working Example
Below is the complete, runnable Python script. Save this as cxone_call_trigger.py.
import requests
import os
import sys
import json
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Configuration
CXONE_CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CXONE_CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
CXONE_BASE_URL = "https://api.nicecxone.com"
def get_access_token() -> str:
"""Retrieves an OAuth 2.0 access token from CXone."""
token_url = f"{CXONE_BASE_URL}/oauth/token"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "client_credentials",
"client_id": CXONE_CLIENT_ID,
"client_secret": CXONE_CLIENT_SECRET,
"scope": "personal-connection:write"
}
try:
response = requests.post(token_url, headers=headers, data=data, timeout=10)
response.raise_for_status()
token_json = response.json()
return token_json["access_token"]
except requests.exceptions.RequestException as e:
print(f"Error obtaining access token: {e}")
sys.exit(1)
def build_personal_connection_payload(
target_number: str,
caller_id: str | None = None,
context_data: dict | None = None
) -> dict:
"""Constructs the JSON payload for a CXone Personal Connection voice call."""
payload = {
"type": "voice",
"target": {
"phoneNumber": target_number
}
}
if caller_id:
payload["callerId"] = caller_id
if context_data:
payload["context"] = context_data
return payload
def trigger_outbound_call(
access_token: str,
target_number: str,
caller_id: str | None = None,
context_data: dict | None = None
) -> dict:
"""Triggers an outbound voice call via the CXone Personal Connection API."""
api_endpoint = f"{CXONE_BASE_URL}/v1/personal-connections"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = build_personal_connection_payload(
target_number=target_number,
caller_id=caller_id,
context_data=context_data
)
print(f"--- Initiating Call ---")
print(f"Target: {target_number}")
print(f"Caller ID: {caller_id or 'Default'}")
try:
response = requests.post(api_endpoint, headers=headers, json=payload, timeout=10)
# Handle specific HTTP errors
if response.status_code == 401:
print("Error: 401 Unauthorized. Check your Client ID/Secret.")
sys.exit(1)
elif response.status_code == 403:
print("Error: 403 Forbidden. Check OAuth scopes (personal-connection:write).")
sys.exit(1)
elif response.status_code == 400:
error_msg = response.json().get("message", "Bad Request")
print(f"Error: 400 Bad Request - {error_msg}")
sys.exit(1)
elif response.status_code == 429:
print("Error: 429 Rate Limit Exceeded. Wait before retrying.")
sys.exit(1)
else:
response.raise_for_status()
result = response.json()
print(f"Success! Personal Connection ID: {result.get('personalConnectionId')}")
print(f"Status: {result.get('status')}")
return result
except requests.exceptions.Timeout:
print("Error: Request timed out.")
sys.exit(1)
except requests.exceptions.ConnectionError:
print("Error: Network connection failed.")
sys.exit(1)
except Exception as e:
print(f"Unexpected error: {e}")
sys.exit(1)
def main():
# Check for required environment variables
if not CXONE_CLIENT_ID or not CXONE_CLIENT_SECRET:
print("Error: CXONE_CLIENT_ID and CXONE_CLIENT_SECRET must be set in .env file.")
sys.exit(1)
# Example Usage
TARGET_NUMBER = "+14155552671" # Replace with a valid E.164 number
CALLER_ID = "+14155550000" # Replace with a verified caller ID in CXone
CONTEXT = {
"campaign_id": "CAMP-001",
"customer_id": "CUST-12345"
}
print("1. Obtaining Access Token...")
token = get_access_token()
print("2. Triggering Outbound Call...")
trigger_outbound_call(
access_token=token,
target_number=TARGET_NUMBER,
caller_id=CALLER_ID,
context_data=CONTEXT
)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
Cause: The OAuth token does not have the required permissions.
Fix: Ensure your OAuth Client in the CXone Admin Console has the personal-connection:write scope assigned. If you are using a custom client, verify that the scope is included in the token request payload.
Error: 400 Bad Request - “Invalid phoneNumber”
Cause: The phone number provided is not in E.164 format or contains invalid characters.
Fix: Ensure the number starts with a plus sign (+) followed by the country code and subscriber number, with no spaces or dashes. Example: +14155552671. Use a library like phonenumbers in Python to validate and format numbers before sending them to the API.
import phonenumbers
def format_e164(phone_number: str) -> str:
parsed_number = phonenumbers.parse(phone_number, None)
return phonenumbers.format_number(parsed_number, phonenumbers.PhoneNumberFormat.E164)
Error: 400 Bad Request - “Caller ID not verified”
Cause: The callerId specified in the payload is not registered or verified in your CXone instance.
Fix: Go to the CXone Admin Console, navigate to Outbound Caller IDs, and ensure the number is added and verified. If you do not specify a callerId, CXone will use the default fallback. Ensure the default fallback is also verified.
Error: 429 Too Many Requests
Cause: You have exceeded the rate limit for the Personal Connection API. CXone enforces rate limits to protect platform stability.
Fix: Implement exponential backoff in your retry logic. The response header Retry-After may indicate how long to wait. For high-volume campaigns, consider using CXone Campaign Manager instead of the Personal Connection API, as the API is designed for ad-hoc or low-volume programmatic calls.