Triggering NICE CXone Personal Connection Calls via the API

Triggering NICE CXone Personal Connection Calls via the API

What You Will Build

  • One sentence: You will build a script that programmatically initiates an outbound voice call to a specific contact using the NICE CXone Personal Connection API.
  • One sentence: This tutorial uses the CXone REST API endpoints for Personal Connection and standard HTTP libraries.
  • One sentence: The implementation is provided in Python using requests and JavaScript using fetch.

Prerequisites

  • OAuth Client Type: You need a valid CXone Organization ID and an OAuth Client ID/Secret with the contactcenter scope.
  • API Version: The Personal Connection API is accessed via the standard CXone v1 endpoints.
  • Language/Runtime: Python 3.8+ or Node.js 16+.
  • External Dependencies:
    • Python: pip install requests
    • Node.js: No external dependencies required (uses built-in fetch in modern Node versions, or node-fetch for older versions).
  • Personal Connection Setup: You must have a Personal Connection account configured in your CXone organization with a valid phone number assigned to the user or the connection profile.

Authentication Setup

CXone uses OAuth 2.0 for authentication. You must obtain a bearer token before making any API calls. The token expires after a set duration (typically 3600 seconds), so your production code should implement caching and refresh logic. For this tutorial, we will fetch a fresh token each time to ensure clarity.

Python Authentication

import requests
import json

def get_cxone_token(org_id: str, client_id: str, client_secret: str) -> str:
    """
    Retrieves an OAuth2 bearer token from CXone.
    """
    url = f"https://{org_id}.mypurecloud.com/api/v2/oauth/token"
    
    # CXone uses client_credentials grant for server-to-server API calls
    payload = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret
    }

    headers = {
        "Content-Type": "application/json"
    }

    try:
        response = requests.post(url, json=payload, headers=headers)
        response.raise_for_status()
        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 Exception as e:
        print(f"An error occurred during authentication: {e}")
        raise

# Usage
ORG_ID = "your-org-id"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"

try:
    ACCESS_TOKEN = get_cxone_token(ORG_ID, CLIENT_ID, CLIENT_SECRET)
    print("Token acquired successfully.")
except Exception:
    print("Failed to acquire token. Aborting.")
    exit(1)

JavaScript Authentication

async function getCxoneToken(orgId, clientId, clientSecret) {
    const url = `https://${orgId}.mypurecloud.com/api/v2/oauth/token`;
    
    const payload = {
        grant_type: 'client_credentials',
        client_id: clientId,
        client_secret: clientSecret
    };

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`Authentication failed: ${response.status} - ${errorText}`);
        }

        const data = await response.json();
        return data.access_token;
    } catch (error) {
        console.error("Authentication error:", error);
        throw error;
    }
}

// Usage
const ORG_ID = "your-org-id";
const CLIENT_ID = "your-client-id";
const CLIENT_SECRET = "your-client-secret";

(async () => {
    try {
        const accessToken = await getCxoneToken(ORG_ID, CLIENT_ID, CLIENT_SECRET);
        console.log("Token acquired successfully.");
        // Proceed with API calls...
    } catch (error) {
        console.error("Failed to acquire token. Aborting.");
        process.exit(1);
    }
})();

Implementation

Step 1: Identify the Personal Connection Account

Before triggering a call, you must know the personalConnectionAccountId. This ID links the API call to the specific Personal Connection profile (phone number) configured in your CXone organization.

You can retrieve the list of available Personal Connection accounts using the GET /api/v2/contactcenter/personalconnections endpoint.

Required Scope: personalconnection:read

Python: List Accounts

import requests

def list_personal_connections(org_id: str, access_token: str) -> list:
    """
    Retrieves all Personal Connection accounts for the organization.
    """
    url = f"https://{org_id}.mypurecloud.com/api/v2/contactcenter/personalconnections"
    
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Accept": "application/json"
    }

    try:
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.HTTPError as e:
        print(f"Failed to list connections: {e.response.status_code}")
        raise

# Example usage to find the ID
# connections = list_personal_connections(ORG_ID, ACCESS_TOKEN)
# target_account_id = connections[0]['id'] # In production, filter by name or number

JavaScript: List Accounts

async function listPersonalConnections(orgId, accessToken) {
    const url = `https://${orgId}.mypurecloud.com/api/v2/contactcenter/personalconnections`;

    const response = await fetch(url, {
        method: 'GET',
        headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Accept': 'application/json'
        }
    });

    if (!response.ok) {
        throw new Error(`Failed to list connections: ${response.status}`);
    }

    return await response.json();
}

Step 2: Trigger the Outbound Call

The core action is performed via the POST /api/v2/contactcenter/personalconnections/{personalConnectionAccountId}/calls endpoint.

Required Scope: personalconnection:write

Request Payload Structure

The request body must contain the to field (the recipient’s phone number) and optionally the from field (if you want to override the default number associated with the account, though usually, the account determines the from number).

JSON Payload Example:

{
    "to": "+15551234567",
    "from": "+15559876543"
}

Note: If from is omitted, the system uses the primary number associated with the personalConnectionAccountId.

Python: Trigger Call

import requests
import json

def trigger_outbound_call(org_id: str, access_token: str, account_id: str, to_number: str, from_number: str = None) -> dict:
    """
    Triggers an outbound call via Personal Connection.
    
    Args:
        org_id: CXone Organization ID
        access_token: OAuth2 Bearer Token
        account_id: The ID of the Personal Connection account
        to_number: The recipient's phone number (E.164 format)
        from_number: Optional. The caller ID number. Defaults to account's primary number.
    
    Returns:
        The response JSON containing the call ID and status.
    """
    url = f"https://{org_id}.mypurecloud.com/api/v2/contactcenter/personalconnections/{account_id}/calls"
    
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
        "Accept": "application/json"
    }

    payload = {
        "to": to_number
    }

    if from_number:
        payload["from"] = from_number

    try:
        response = requests.post(url, json=payload, headers=headers)
        
        # Handle common errors
        if response.status_code == 401:
            print("Error: Unauthorized. Token may be expired or invalid.")
            raise Exception("Unauthorized")
        elif response.status_code == 403:
            print("Error: Forbidden. Check if 'personalconnection:write' scope is granted.")
            raise Exception("Forbidden")
        elif response.status_code == 404:
            print("Error: Not Found. The Personal Connection Account ID does not exist.")
            raise Exception("Not Found")
        elif response.status_code == 429:
            print("Error: Too Many Requests. Rate limit exceeded.")
            raise Exception("Rate Limit Exceeded")
        
        response.raise_for_status()
        return response.json()
        
    except requests.exceptions.HTTPError as e:
        print(f"HTTP Error: {e.response.status_code} - {e.response.text}")
        raise
    except Exception as e:
        print(f"An error occurred: {e}")
        raise

JavaScript: Trigger Call

async function triggerOutboundCall(orgId, accessToken, accountId, toNumber, fromNumber = null) {
    const url = `https://${orgId}.mypurecloud.com/api/v2/contactcenter/personalconnections/${accountId}/calls`;

    const payload = {
        to: toNumber
    };

    if (fromNumber) {
        payload.from = fromNumber;
    }

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${accessToken}`,
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            },
            body: JSON.stringify(payload)
        });

        if (response.status === 401) {
            throw new Error("Unauthorized. Token may be expired.");
        } else if (response.status === 403) {
            throw new Error("Forbidden. Check OAuth scopes.");
        } else if (response.status === 404) {
            throw new Error("Not Found. Invalid Personal Connection Account ID.");
        } else if (response.status === 429) {
            throw new Error("Rate Limit Exceeded.");
        }

        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`API Error: ${response.status} - ${errorText}`);
        }

        return await response.json();
    } catch (error) {
        console.error("Call trigger error:", error);
        throw error;
    }
}

Step 3: Processing the Response

The API returns a 200 OK response with a JSON body containing the details of the initiated call. The most critical field is callId, which you can use for subsequent lookups or logging.

Successful Response Example:

{
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "personalConnectionAccountId": "pc-12345",
    "to": "+15551234567",
    "from": "+15559876543",
    "status": "initiated",
    "createdTime": "2023-10-27T10:00:00.000Z"
}

You should log the id field. This ID corresponds to the call instance in CXone. You can use this ID to fetch call details later if you need to verify if the call was answered, missed, or failed.

Python: Handling Response

# Continuing from trigger_outbound_call function
call_result = trigger_outbound_call(ORG_ID, ACCESS_TOKEN, target_account_id, "+15551234567")

if call_result:
    call_id = call_result.get("id")
    status = call_result.get("status")
    print(f"Call initiated successfully. ID: {call_id}, Status: {status}")
    
    # In a production environment, you might store this call_id in a database
    # to correlate with future webhooks or status checks.

JavaScript: Handling Response

// Continuing from triggerOutboundCall function
const callResult = await triggerOutboundCall(ORG_ID, accessToken, targetAccountId, "+15551234567");

if (callResult) {
    const callId = callResult.id;
    const status = callResult.status;
    console.log(`Call initiated successfully. ID: ${callId}, Status: ${status}`);
}

Complete Working Example

Below is a complete, runnable Python script. Replace the placeholder credentials with your actual CXone details.

import requests
import sys

# --- Configuration ---
ORG_ID = "your-org-id"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
PERSONAL_CONNECTION_ACCOUNT_ID = "your-pc-account-id"
RECIPIENT_PHONE_NUMBER = "+15551234567"
# Optional: Caller ID. If None, uses the PC account's default number.
CALLER_ID_NUMBER = None 

# --- Functions ---

def get_token():
    url = f"https://{ORG_ID}.mypurecloud.com/api/v2/oauth/token"
    payload = {
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET
    }
    headers = {"Content-Type": "application/json"}
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        response.raise_for_status()
        return response.json()["access_token"]
    except Exception as e:
        print(f"Failed to get token: {e}")
        sys.exit(1)

def make_call(access_token):
    url = f"https://{ORG_ID}.mypurecloud.com/api/v2/contactcenter/personalconnections/{PERSONAL_CONNECTION_ACCOUNT_ID}/calls"
    
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
        "Accept": "application/json"
    }

    payload = {
        "to": RECIPIENT_PHONE_NUMBER
    }

    if CALLER_ID_NUMBER:
        payload["from"] = CALLER_ID_NUMBER

    try:
        response = requests.post(url, json=payload, headers=headers)
        
        if response.status_code == 200:
            result = response.json()
            print(f"SUCCESS: Call initiated. ID: {result['id']}, Status: {result['status']}")
            return result
        else:
            print(f"FAILED: Status Code {response.status_code}")
            print(f"Response: {response.text}")
            return None
            
    except Exception as e:
        print(f"Error making call: {e}")
        return None

# --- Main Execution ---

if __name__ == "__main__":
    print("1. Authenticating...")
    token = get_token()
    
    print("2. Initiating Outbound Call...")
    make_call(token)
    
    print("3. Done.")

Common Errors & Debugging

Error: 401 Unauthorized

  • What causes it: The OAuth token is expired, invalid, or malformed.
  • How to fix it: Ensure your token fetch logic is correct. Check that the client_id and client_secret match the OAuth Client in your CXone Admin Console. Verify that the token is being passed in the header as Bearer <token>.

Error: 403 Forbidden

  • What causes it: The OAuth Client does not have the required scope.
  • How to fix it: Go to the CXone Admin Console > Platform > OAuth Clients. Select your client and ensure the personalconnection:write scope is checked. Note that scope changes may require re-issuing the token or waiting for propagation.

Error: 404 Not Found

  • What causes it: The personalConnectionAccountId provided in the URL does not exist or is not accessible by the user associated with the OAuth client.
  • How to fix it: Use the GET /api/v2/contactcenter/personalconnections endpoint to list all available accounts and copy the correct id. Ensure the account is active.

Error: 400 Bad Request

  • What causes it: The phone number format is invalid or missing required fields.
  • How to fix it: Ensure the to number is in E.164 format (e.g., +15551234567). The + sign is required. Check that the JSON payload is valid.

Error: 429 Too Many Requests

  • What causes it: You have exceeded the rate limit for the Personal Connection API.
  • How to fix it: Implement exponential backoff in your code. The response headers usually include Retry-After. Do not retry immediately; wait for the specified duration.

Official References