Trigger NICE CXone Outbound Calls with the Personal Connection API

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 requests library 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:outbound scope. If you are triggering calls on behalf of a specific agent, you may also need agents:read to verify agent status.
  • SDK/API Version: CXone REST API v2.
  • Language/Runtime: Python 3.8+.
  • External Dependencies: requests library (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: Usually voice for outbound calls.
  • callType: Typically OUTBOUND.

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 CxoneAuth class 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:outbound scope, or the Agent does not have permission to make outbound calls.
  • Fix:
    1. Go to the CXone Developer Portal.
    2. Edit your OAuth Client.
    3. Ensure the Scopes tab includes calls:outbound.
    4. 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:
    1. Log in to the CXone Agent Desktop as the target agent.
    2. Ensure the agent is in the Available state.
    3. If using a specific Outbound Campaign, ensure the agent is assigned to that campaign.
    4. Check the description field 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.contact is 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-After headers in some cases, but generally, spacing requests by 1-2 seconds is recommended for bulk operations.

Official References