Initiate an Outbound Call on Behalf of an Agent via Genesys Cloud API

Initiate an Outbound Call on Behalf of an Agent via Genesys Cloud API

What You Will Build

  • This tutorial demonstrates how to programmatically initiate an outbound call from a Genesys Cloud user (agent) to an external phone number using the REST API.
  • The solution utilizes the POST /api/v2/conversations/calls endpoint to create a new voice conversation.
  • The implementation is provided in Python using the official genesys-cloud-purecloud-sdk and in Node.js using the @genesyscloud/genesys-cloud-purecloud-sdk.

Prerequisites

  • OAuth Client: A Genesys Cloud application with the Client Credentials grant type.
  • Required Scopes:
    • conversation:call:create (Required to initiate the call)
    • user:read (Optional, if you need to look up user details before calling)
  • SDK Versions:
    • Python: genesys-cloud-purecloud-sdk >= 2.0.0
    • Node.js: @genesyscloud/genesys-cloud-purecloud-sdk >= 1.0.0
  • Runtime:
    • Python 3.8+
    • Node.js 14+
  • Dependencies:
    • Python: pip install genesys-cloud-purecloud-sdk
    • Node.js: npm install @genesyscloud/genesys-cloud-purecloud-sdk

Authentication Setup

Genesys Cloud APIs use OAuth 2.0 for authentication. For server-to-server integrations (like an outbound dialer or backend service), the Client Credentials flow is the standard approach. This flow exchanges your Client ID and Client Secret for an access token valid for one hour.

Python Authentication

import os
from purecloud_platform_client import Configuration, ApiClient
import purecloud_platform_client.rest as rest

def get_purecloud_api_client():
    """
    Configures and returns a PureCloud API client instance using Client Credentials flow.
    """
    # Load credentials from environment variables
    client_id = os.environ.get("GENESYS_CLIENT_ID")
    client_secret = os.environ.get("GENESYS_CLIENT_SECRET")
    
    if not client_id or not client_secret:
        raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")

    # Initialize the configuration
    config = Configuration()
    config.host = "https://api.mypurecloud.com" # Adjust region if necessary (e.g., api.us.genesyscloud.com)
    config.oauth_client_id = client_id
    config.oauth_client_secret = client_secret
    
    # Create the API client
    api_client = ApiClient(configuration=config)
    
    # Perform the OAuth token exchange
    # This automatically handles token storage and refresh within the session
    try:
        api_client.reauthenticate()
    except Exception as e:
        raise RuntimeError(f"Failed to authenticate with Genesys Cloud: {e}")

    return api_client

Node.js Authentication

import { Configuration, ApiClient } from '@genesyscloud/genesys-cloud-purecloud-sdk';

/**
 * Configures and returns a Genesys Cloud API client instance using Client Credentials flow.
 */
export async function getGenesysApiClient() {
    const clientId = process.env.GENESYS_CLIENT_ID;
    const clientSecret = process.env.GENESYS_CLIENT_SECRET;

    if (!clientId || !clientSecret) {
        throw new Error('GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.');
    }

    const config = new Configuration({
        basePath: 'https://api.mypurecloud.com', // Adjust region if necessary
        clientId: clientId,
        clientSecret: clientSecret
    });

    const apiClient = new ApiClient(config);

    try {
        // Perform the OAuth token exchange
        await apiClient.reauthenticate();
    } catch (error) {
        throw new Error(`Failed to authenticate with Genesys Cloud: ${error.message}`);
    }

    return apiClient;
}

Implementation

Step 1: Construct the Outbound Call Request

To initiate a call, you must send a POST request to /api/v2/conversations/calls. The request body must contain a to object representing the recipient and optionally a from object representing the caller identity.

Critical Parameter: from
The from field determines who is making the call. It can be:

  1. A User: Identified by the user’s Genesys Cloud ID or email. This routes the call through that user’s configured DID (Direct Inbound Dialing) number.
  2. A Station: Identified by the station ID. This uses the specific phone line associated with that station.
  3. An Outbound Route: If you have configured outbound routes, you can specify the route ID to ensure the call leaves via the correct carrier.

For this tutorial, we will use a User as the caller identity. This is the most common pattern for “on behalf of” scenarios.

Python Implementation

from purecloud_platform_client import (
    CallsApi,
    CallConversationRequest,
    ConversationParticipantRequest,
    ConversationParticipantRequestFrom,
    ConversationParticipantRequestTo
)

def initiate_outbound_call(api_client, caller_user_id, recipient_phone_number):
    """
    Initiates an outbound call from a specific user to a phone number.
    
    Args:
        api_client: The authenticated PureCloud API client.
        caller_user_id (str): The Genesys Cloud User ID of the agent making the call.
        recipient_phone_number (str): The E.164 formatted phone number to call (e.g., "+15551234567").
    """
    calls_api = CallsApi(api_client)

    # 1. Define the 'from' participant (The Agent)
    # Using user_id ensures the call originates from the user's assigned DID.
    from_participant = ConversationParticipantRequestFrom(
        user_id=caller_user_id
    )

    # 2. Define the 'to' participant (The Recipient)
    # The phone number MUST be in E.164 format for reliable routing.
    to_participant = ConversationParticipantRequestTo(
        phone_number=recipient_phone_number
    )

    # 3. Construct the Call Conversation Request
    call_request = CallConversationRequest(
        from_=from_participant,
        to=to_participant
    )

    try:
        # 4. Execute the API call
        response = calls_api.post_conversations_calls(body=call_request)
        
        # The response contains the conversation ID
        conversation_id = response.conversation_id
        print(f"Call initiated successfully. Conversation ID: {conversation_id}")
        return conversation_id

    except Exception as e:
        # Handle API errors
        if hasattr(e, 'status') and e.status == 400:
            print(f"Bad Request: Check phone number format or user permissions. Details: {e.body}")
        elif hasattr(e, 'status') and e.status == 403:
            print(f"Forbidden: User lacks permissions or is not in a valid state (e.g., offline). Details: {e.body}")
        else:
            print(f"Error initiating call: {e}")
        raise

Node.js Implementation

import { CallsApi, CallConversationRequest, ConversationParticipantRequestFrom, ConversationParticipantRequestTo } from '@genesyscloud/genesys-cloud-purecloud-sdk';

/**
 * Initiates an outbound call from a specific user to a phone number.
 * 
 * @param {ApiClient} apiClient - The authenticated Genesys Cloud API client.
 * @param {string} callerUserId - The Genesys Cloud User ID of the agent making the call.
 * @param {string} recipientPhoneNumber - The E.164 formatted phone number to call (e.g., "+15551234567").
 * @returns {Promise<string>} The Conversation ID.
 */
export async function initiateOutboundCall(apiClient, callerUserId, recipientPhoneNumber) {
    const callsApi = new CallsApi(apiClient);

    // 1. Define the 'from' participant (The Agent)
    // Using userId ensures the call originates from the user's assigned DID.
    const fromParticipant = new ConversationParticipantRequestFrom({
        userId: callerUserId
    });

    // 2. Define the 'to' participant (The Recipient)
    // The phone number MUST be in E.164 format for reliable routing.
    const toParticipant = new ConversationParticipantRequestTo({
        phoneNumber: recipientPhoneNumber
    });

    // 3. Construct the Call Conversation Request
    const callRequest = new CallConversationRequest({
        from: fromParticipant,
        to: toParticipant
    });

    try {
        // 4. Execute the API call
        const response = await callsApi.postConversationsCalls({ body: callRequest });
        
        const conversationId = response.body.conversationId;
        console.log(`Call initiated successfully. Conversation ID: ${conversationId}`);
        return conversationId;

    } catch (error) {
        // Handle API errors
        if (error.status === 400) {
            console.error(`Bad Request: Check phone number format or user permissions. Details: ${JSON.stringify(error.body)}`);
        } else if (error.status === 403) {
            console.error(`Forbidden: User lacks permissions or is not in a valid state (e.g., offline). Details: ${JSON.stringify(error.body)}`);
        } else {
            console.error(`Error initiating call:`, error.message);
        }
        throw error;
    }
}

Step 2: Understanding the Response and Edge Cases

The POST /api/v2/conversations/calls endpoint returns a 201 Created status code upon successful initiation. The response body is a CallConversationResponse object.

Key Response Fields:

  • conversationId: The unique identifier for this call session. You will need this ID to monitor the call status, transfer the call, or hang up programmatically.
  • participants: An array containing the initial participants. Note that the from participant (the agent) is automatically added to this list.

Edge Case: User State
If the user specified in the from field is Offline or Not Available, the call will typically fail with a 400 Bad Request or 403 Forbidden error, depending on your organization’s routing policies. To mitigate this, you should check the user’s presence before initiating the call.

Checking User Presence (Python)

from purecloud_platform_client import UsersApi

def check_user_presence(api_client, user_id):
    """
    Checks if the user is currently available to make calls.
    """
    users_api = UsersApi(api_client)
    try:
        user_response = users_api.get_user(user_id=user_id)
        # Presence is not directly in the User object, but we can infer from the last updated time or use the Presence API
        # However, a simpler check is to see if the user has a station configured and is licensed for voice.
        if not user_response.licensed_features or 'voice' not in [f.name for f in user_response.licensed_features]:
            return False, "User does not have a voice license."
        
        # A more robust check involves the Presence API, but for simplicity, we assume valid configuration.
        return True, "User is valid."
    except Exception as e:
        return False, f"Error checking user: {e}"

Step 3: Monitoring Call Status

Once the call is initiated, it enters a queued or connecting state. To monitor the progress, you can use the GET /api/v2/conversations/calls/{conversationId} endpoint.

Python Monitoring Example

import time

def monitor_call_status(api_client, conversation_id, timeout_seconds=60):
    """
    Polls the call status until it connects, fails, or times out.
    """
    calls_api = CallsApi(api_client)
    start_time = time.time()

    while time.time() - start_time < timeout_seconds:
        try:
            call_response = calls_api.get_conversations_call(conversation_id=conversation_id)
            
            # Check the state of the call
            # The 'participants' list contains the current state of each leg
            if call_response.participants:
                # Typically, the first participant is the caller, second is the callee
                # We look for the 'to' participant's state
                for participant in call_response.participants:
                    if participant.to and participant.to.phone_number:
                        print(f"Call State: {participant.state}")
                        if participant.state in ['connected', 'disconnected', 'failed', 'busy']:
                            return participant.state
            
            time.sleep(2) # Wait 2 seconds before polling again

        except Exception as e:
            print(f"Error polling call status: {e}")
            return "error"

    return "timeout"

Complete Working Example

Below is a complete, runnable Python script that authenticates, checks user validity, initiates the call, and monitors its initial status.

import os
import sys
import time
from purecloud_platform_client import (
    Configuration,
    ApiClient,
    CallsApi,
    UsersApi,
    CallConversationRequest,
    ConversationParticipantRequestFrom,
    ConversationParticipantRequestTo
)

def main():
    # 1. Setup Authentication
    client_id = os.environ.get("GENESYS_CLIENT_ID")
    client_secret = os.environ.get("GENESYS_CLIENT_SECRET")
    
    if not client_id or not client_secret:
        print("Error: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")
        sys.exit(1)

    config = Configuration()
    config.host = "https://api.mypurecloud.com"
    config.oauth_client_id = client_id
    config.oauth_client_secret = client_secret
    
    api_client = ApiClient(configuration=config)
    try:
        api_client.reauthenticate()
    except Exception as e:
        print(f"Authentication failed: {e}")
        sys.exit(1)

    # 2. Define Call Parameters
    caller_user_id = "YOUR_USER_ID_HERE" # Replace with actual User ID
    recipient_phone_number = "+15551234567" # Replace with E.164 number

    if caller_user_id == "YOUR_USER_ID_HERE":
        print("Error: Please replace YOUR_USER_ID_HERE with a valid Genesys Cloud User ID.")
        sys.exit(1)

    # 3. Validate User (Optional but Recommended)
    users_api = UsersApi(api_client)
    try:
        user_response = users_api.get_user(user_id=caller_user_id)
        print(f"Initiating call on behalf of: {user_response.name}")
    except Exception as e:
        print(f"User validation failed: {e}")
        sys.exit(1)

    # 4. Initiate Call
    calls_api = CallsApi(api_client)
    
    from_participant = ConversationParticipantRequestFrom(user_id=caller_user_id)
    to_participant = ConversationParticipantRequestTo(phone_number=recipient_phone_number)
    call_request = CallConversationRequest(from_=from_participant, to=to_participant)

    try:
        response = calls_api.post_conversations_calls(body=call_request)
        conversation_id = response.conversation_id
        print(f"Call initiated successfully. Conversation ID: {conversation_id}")
        
        # 5. Monitor Initial Status
        print("Monitoring call status...")
        time.sleep(5) # Give it a few seconds to process
        
        try:
            call_status = calls_api.get_conversations_call(conversation_id=conversation_id)
            if call_status.participants:
                for p in call_status.participants:
                    print(f"Participant: {p.from_} -> {p.to}, State: {p.state}")
        except Exception as e:
            print(f"Error fetching status: {e}")

    except Exception as e:
        if hasattr(e, 'status'):
            print(f"API Error {e.status}: {e.body}")
        else:
            print(f"Error: {e}")

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 400 Bad Request - “Invalid phone number format”

  • Cause: The phone_number field in the to object is not in E.164 format.
  • Fix: Ensure the phone number starts with a + followed by the country code and number (e.g., +14155552671). Do not include spaces, dashes, or parentheses.

Error: 403 Forbidden - “User is not in a valid state”

  • Cause: The user specified in the from field is currently Offline, Lunch, or Break. Genesys Cloud does not allow outbound calls to originate from users who are not in an Available or Working state.
  • Fix: Use the Presence API to check the user’s status before initiating the call. Alternatively, use a Station ID or Outbound Route ID in the from object instead of a user_id to bypass user presence requirements.

Error: 403 Forbidden - “Insufficient permissions”

  • Cause: The OAuth token does not have the conversation:call:create scope.
  • Fix: Update your Genesys Cloud application settings to include the conversation:call:create scope and re-authenticate.

Error: 429 Too Many Requests

  • Cause: You have exceeded the rate limit for the POST /api/v2/conversations/calls endpoint.
  • Fix: Implement exponential backoff retry logic. The response header Retry-After indicates the number of seconds to wait before retrying.

Official References