Triggering NICE CXone Personal Connection Outbound Calls via API
What You Will Build
- You will build a script that authenticates to NICE CXone and triggers an outbound call to a specified telephone number.
- You will use the NICE CXone Personal Connection API (
/api/v2/personal-connection/calls) to initiate the call flow. - You will implement this in Python using the
requestslibrary to handle OAuth token acquisition and HTTP POST requests.
Prerequisites
- OAuth Client Type: Service Account (Client Credentials Grant) or User Account (Resource Owner Password Credentials / Authorization Code). This tutorial uses Client Credentials for server-to-server automation.
- Required Scopes:
personal-connection:writeis required to initiate calls.personal-connection:readis useful for debugging status. - API Version: v2.
- Runtime Requirements: Python 3.8+.
- External Dependencies:
requests(for HTTP interactions)python-dotenv(for secure credential management)
Install dependencies:
pip install requests python-dotenv
Authentication Setup
NICE CXone APIs require a valid OAuth 2.0 access token. You must obtain this token from the /v2/oauth/token endpoint before making any API calls. The token expires after a set period (typically 3600 seconds), so your application must handle token refresh or re-acquisition.
For this tutorial, we assume you have created a Service Account in the NICE CXone Admin Portal with the necessary permissions.
Step 1: Obtain Access Token
The following Python code demonstrates how to retrieve an access token using the Client Credentials flow.
import requests
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Configuration from environment variables
REALM = os.getenv("CXONE_REALM") # e.g., "us-east-1"
CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
# Base URL for NICE CXone API
API_BASE_URL = f"https://{REALM}.api.niceincontact.com"
def get_access_token() -> str:
"""
Retrieves an OAuth 2.0 access token from NICE CXone.
Returns:
str: The access token.
Raises:
requests.exceptions.HTTPError: If authentication fails.
"""
token_url = f"{API_BASE_URL}/v2/oauth/token"
# The grant type for service accounts is 'client_credentials'
payload = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
try:
response = requests.post(token_url, data=payload)
response.raise_for_status() # Raise exception for 4xx/5xx responses
token_data = response.json()
return token_data["access_token"]
except requests.exceptions.HTTPError as e:
print(f"Authentication failed: {e.response.status_code} - {e.response.text}")
raise
except KeyError:
print("Invalid token response structure.")
raise ValueError("Token response did not contain 'access_token'.")
# Test the function
if __name__ == "__main__":
token = get_access_token()
print("Token acquired successfully.")
Important Note on Scopes: If your service account does not have the personal-connection:write scope assigned, the token request might still succeed, but subsequent API calls will return a 403 Forbidden error. Ensure the scope is attached to the client in the Admin Portal.
Implementation
Step 2: Constructing the Personal Connection Call Request
To trigger a call, you must send a POST request to the /v2/personal-connection/calls endpoint. The request body must contain a JSON object specifying the caller ID, the called number, and the flow to execute.
Key Parameters
callerNumber: The phone number that appears on the recipient’s caller ID. This must be a valid, provisioned number in your CXone account.calledNumber: The destination phone number.flowId: The unique identifier of the Flow Studio flow you want to execute. This flow must be published and configured to handle inbound calls (even though this is outbound, the flow logic executes as if the call was answered).parameters(Optional): A dictionary of key-value pairs passed to the flow. This allows you to customize the interaction dynamically (e.g., passing a customer name or account ID).
Error Handling for 429 Rate Limits
NICE CXone APIs enforce rate limits. If you exceed the limit, you will receive a 429 Too Many Requests response. Your code should implement exponential backoff to retry the request.
Step 3: Implementing the Call Trigger with Retry Logic
The following function encapsulates the logic to trigger the call. It includes retry logic for rate limiting and proper error handling for common HTTP status codes.
import time
import json
def trigger_personal_connection_call(
access_token: str,
caller_number: str,
called_number: str,
flow_id: str,
parameters: dict = None
) -> dict:
"""
Triggers an outbound call using NICE CXone Personal Connection API.
Args:
access_token: Valid OAuth access token.
caller_number: The outbound caller ID (E.164 format).
called_number: The destination number (E.164 format).
flow_id: The ID of the Flow Studio flow to execute.
parameters: Optional dictionary of flow parameters.
Returns:
dict: The API response containing the call ID and status.
Raises:
requests.exceptions.HTTPError: If the API returns an error status.
"""
url = f"{API_BASE_URL}/v2/personal-connection/calls"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
# Construct the request body
body = {
"callerNumber": caller_number,
"calledNumber": called_number,
"flowId": flow_id
}
if parameters:
body["parameters"] = parameters
# Retry logic for 429 Too Many Requests
max_retries = 3
retry_delay = 2 # Initial delay in seconds
for attempt in range(max_retries):
try:
response = requests.post(url, headers=headers, json=body)
# Check for success
if response.status_code == 200:
return response.json()
# Handle Rate Limiting (429)
elif response.status_code == 429:
if attempt < max_retries - 1:
print(f"Rate limited (429). Retrying in {retry_delay} seconds...")
time.sleep(retry_delay)
retry_delay *= 2 # Exponential backoff
continue
else:
raise requests.exceptions.RetryError("Max retries exceeded for 429.")
# Handle other HTTP errors
else:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
error_body = e.response.text
print(f"HTTP Error {e.response.status_code}: {error_body}")
# Specific handling for common errors
if e.response.status_code == 401:
raise Exception("Authentication failed. Token may be expired.")
elif e.response.status_code == 403:
raise Exception("Forbidden. Check if 'personal-connection:write' scope is granted.")
elif e.response.status_code == 400:
raise Exception(f"Bad Request. Check phone number formats and flow ID. Details: {error_body}")
raise
return None
Step 4: Processing Results
The API returns a JSON object immediately upon accepting the request. This response contains the callId, which is crucial for tracking the call’s lifecycle.
Expected Response Body (200 OK):
{
"callId": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
"status": "queued",
"timestamp": "2023-10-27T10:15:30.123Z"
}
callId: A unique identifier for this specific call instance. Use this with the/v2/personal-connection/calls/{callId}endpoint to check real-time status (ringing, answered, completed, failed).status: Initial status is usuallyqueuedorinitiated. It will change as the call progresses.
Complete Working Example
Below is a complete, runnable Python script that combines authentication and call triggering. Save this as cxone_call_trigger.py.
Prerequisites:
-
Create a
.envfile in the same directory with the following variables:CXONE_REALM=us-east-1 CXONE_CLIENT_ID=your_client_id_here CXONE_CLIENT_SECRET=your_client_secret_here CXONE_CALLER_NUMBER=+15551234567 CXONE_FLOW_ID=your_published_flow_id_here CXONE_DESTINATION_NUMBER=+15559876543 -
Ensure the
CXONE_CALLER_NUMBERis a valid, verified number in your CXone account. -
Ensure the
CXONE_FLOW_IDis a published flow that can handle calls.
import requests
import os
import time
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Configuration
REALM = os.getenv("CXONE_REALM")
CLIENT_ID = os.getenv("CXONE_CLIENT_ID")
CLIENT_SECRET = os.getenv("CXONE_CLIENT_SECRET")
CALLER_NUMBER = os.getenv("CXONE_CALLER_NUMBER")
DESTINATION_NUMBER = os.getenv("CXONE_DESTINATION_NUMBER")
FLOW_ID = os.getenv("CXONE_FLOW_ID")
API_BASE_URL = f"https://{REALM}.api.niceincontact.com"
def get_access_token() -> str:
"""Retrieves an OAuth 2.0 access token."""
token_url = f"{API_BASE_URL}/v2/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
try:
response = requests.post(token_url, data=payload)
response.raise_for_status()
return response.json()["access_token"]
except requests.exceptions.HTTPError as e:
print(f"Authentication failed: {e.response.status_code} - {e.response.text}")
raise
def trigger_call(access_token: str) -> dict:
"""Triggers an outbound call via Personal Connection API."""
url = f"{API_BASE_URL}/v2/personal-connection/calls"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
body = {
"callerNumber": CALLER_NUMBER,
"calledNumber": DESTINATION_NUMBER,
"flowId": FLOW_ID,
"parameters": {
"greeting": "Hello from API",
"agent_name": "System Bot"
}
}
max_retries = 3
retry_delay = 2
for attempt in range(max_retries):
try:
response = requests.post(url, headers=headers, json=body)
if response.status_code == 200:
result = response.json()
print("Call triggered successfully!")
print(f"Call ID: {result.get('callId')}")
print(f"Status: {result.get('status')}")
return result
elif response.status_code == 429:
if attempt < max_retries - 1:
print(f"Rate limited. Retrying in {retry_delay}s...")
time.sleep(retry_delay)
retry_delay *= 2
continue
else:
raise Exception("Max retries exceeded due to rate limiting.")
else:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error {e.response.status_code}: {e.response.text}")
if e.response.status_code == 403:
print("Tip: Ensure your service account has the 'personal-connection:write' scope.")
elif e.response.status_code == 400:
print("Tip: Verify phone numbers are in E.164 format and Flow ID is correct.")
raise
return None
def main():
if not all([REALM, CLIENT_ID, CLIENT_SECRET, CALLER_NUMBER, DESTINATION_NUMBER, FLOW_ID]):
print("Error: Missing environment variables. Please check your .env file.")
return
try:
token = get_access_token()
trigger_call(token)
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
Cause: The OAuth token used for the request does not have the required scope.
Fix:
- Log in to the NICE CXone Admin Portal.
- Navigate to Administration > Security > OAuth Clients.
- Find your Client ID.
- Edit the client and ensure
personal-connection:writeis checked under Scopes. - Regenerate the token and retry.
Error: 400 Bad Request - “Invalid Flow ID”
Cause: The flowId provided in the request body does not exist, is not published, or is not accessible to the user/service account.
Fix:
- Verify the Flow ID in Flow Studio.
- Ensure the flow is Published. Draft flows cannot be triggered via API.
- Check if the service account has permissions to access that specific flow.
Error: 400 Bad Request - “Invalid Caller Number”
Cause: The callerNumber is not provisioned in your CXone account, or the format is incorrect.
Fix:
- Ensure the caller number is in E.164 format (e.g.,
+15551234567). - Verify the number is added in Administration > Phone Numbers.
- Ensure the number is not blocked or suspended.
Error: 429 Too Many Requests
Cause: You have exceeded the API rate limit for your account or tenant.
Fix:
- Implement exponential backoff in your code (as shown in the complete example).
- Review your integration logic to ensure you are not making redundant calls.
- Contact NICE Support if you require a higher rate limit for high-volume campaigns.