Triggering NICE CXone Personal Connection Outbound Calls via API

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 requests library 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:write is required to initiate calls. personal-connection:read is 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 usually queued or initiated. 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:

  1. Create a .env file 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
    
  2. Ensure the CXONE_CALLER_NUMBER is a valid, verified number in your CXone account.

  3. Ensure the CXONE_FLOW_ID is 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:

  1. Log in to the NICE CXone Admin Portal.
  2. Navigate to Administration > Security > OAuth Clients.
  3. Find your Client ID.
  4. Edit the client and ensure personal-connection:write is checked under Scopes.
  5. 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:

  1. Verify the Flow ID in Flow Studio.
  2. Ensure the flow is Published. Draft flows cannot be triggered via API.
  3. 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:

  1. Ensure the caller number is in E.164 format (e.g., +15551234567).
  2. Verify the number is added in Administration > Phone Numbers.
  3. 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:

  1. Implement exponential backoff in your code (as shown in the complete example).
  2. Review your integration logic to ensure you are not making redundant calls.
  3. Contact NICE Support if you require a higher rate limit for high-volume campaigns.

Official References