Initiating an Outbound Call on Behalf of an Agent Using POST /api/v2/conversations/calls

Initiating an Outbound Call on Behalf of an Agent Using POST /api/v2/conversations/calls

What You Will Build

  • You will write a script that programmatically initiates an outbound voice call from a Genesys Cloud user to an external telephone number.
  • This uses the Genesys Cloud Platform API v2 endpoint POST /api/v2/conversations/calls.
  • The tutorial covers Python, JavaScript, and C# implementations.

Prerequisites

  • OAuth Client Type: A Service Account or Machine-to-Machine (M2M) client with sufficient permissions.
  • Required OAuth Scopes:
    • conversation:call:write (to initiate the call)
    • conversation:call:read (to verify the call state)
    • user:read (optional, to verify the agent user ID)
  • SDK Version:
    • Python: genesyscloud >= 13.0.0
    • JavaScript: @genesyscloud/genesyscloud >= 13.0.0
    • C#: GenesysCloud >= 13.0.0
  • Runtime Requirements:
    • Python 3.8+
    • Node.js 18+
    • .NET 6+
  • External Dependencies:
    • Python: pip install genesyscloud
    • Node: npm install @genesyscloud/genesyscloud
    • C#: Install-Package GenesysCloud

Authentication Setup

Genesys Cloud uses OAuth 2.0 for authentication. For server-to-server integrations, the Client Credentials flow is standard. You must configure your OAuth client in the Genesys Cloud Admin Console under Setup > Integration > OAuth clients.

Below is the authentication logic for Python. The same pattern applies to other languages using their respective SDKs or HTTP libraries.

Python Authentication Example

import os
from genesyscloud.auth import OAuthClient
from genesyscloud.rest import ApiException

def get_authenticated_oauth_client() -> OAuthClient:
    """
    Initializes and authenticates the OAuth client using M2M credentials.
    """
    client_id = os.environ.get("GENESYS_CLOUD_CLIENT_ID")
    client_secret = os.environ.get("GENESYS_CLOUD_CLIENT_SECRET")
    base_url = os.environ.get("GENESYS_CLOUD_BASE_URL", "https://api.mypurecloud.com")

    if not client_id or not client_secret:
        raise ValueError("GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET must be set.")

    oauth_client = OAuthClient(
        client_id=client_id,
        client_secret=client_secret,
        base_url=base_url
    )

    try:
        oauth_client.authenticate()
        print("Authentication successful.")
        return oauth_client
    except ApiException as e:
        print(f"Authentication failed: {e.status} {e.reason}")
        raise

# Usage
# oauth_client = get_authenticated_oauth_client()

Implementation

Step 1: Identify the Agent and Target

Before initiating the call, you must identify the User ID of the agent who will handle the call. This user must be logged into the Genesys Cloud desktop application and have an active presence (e.g., “Available” or “Busy”) that allows inbound or outbound interactions, depending on your routing configuration.

You also need the To number (the external party) and the From number (the Genesys Cloud DID assigned to the user or team).

Python: Fetching User Details

If you only know the agent’s name, you can search for them. However, for production code, always cache the User ID.

from genesyscloud.users import UsersApi

def find_user_by_name(oauth_client: OAuthClient, user_name: str) -> str:
    """
    Finds a user ID by their name.
    Returns the first match found.
    """
    users_api = UsersApi(oauth_client=oauth_client)
    
    try:
        # Search for users with the given name
        response = users_api.post_users_search(
            body={
                "query": user_name,
                "pageSize": 1
            }
        )
        
        if response.entities and len(response.entities) > 0:
            user_id = response.entities[0].id
            print(f"Found User ID: {user_id} for name: {user_name}")
            return user_id
        else:
            raise ValueError(f"No user found with name: {user_name}")
            
    except ApiException as e:
        print(f"Error searching for user: {e.status} {e.reason}")
        raise

# Usage
# agent_id = find_user_by_name(oauth_client, "John Doe")

Step 2: Constructing the Call Request Body

The core of this tutorial is the request body sent to POST /api/v2/conversations/calls. The structure differs slightly from a standard POST /api/v2/conversations call because it is specifically for voice.

Key fields:

  • to: The recipient’s phone number. Must be in E.164 format (e.g., +15551234567).
  • from: The caller ID. This must be a DID provisioned in your Genesys Cloud environment and associated with the user or a team.
  • userId: The ID of the agent who will receive the call.
  • type: Set to "voice".

Python: Building the Request

from genesyscloud.conversations import ConversationsApi
from genesyscloud.conversations.models import ConversationCall

def prepare_call_request(agent_id: str, to_number: str, from_number: str) -> ConversationCall:
    """
    Constructs the ConversationCall object for the API request.
    """
    call_request = ConversationCall(
        to=to_number,
        from_=from_number,  # Note: 'from' is a reserved keyword in Python, so SDK uses from_
        user_id=agent_id,
        type="voice"
    )
    return call_request

Step 3: Initiating the Call

You will use the ConversationsApi to send the request. The API returns a 201 Created status with the Conversation ID and the Participant ID for the agent.

Python: Executing the Call

def initiate_outbound_call(oauth_client: OAuthClient, agent_id: str, to_number: str, from_number: str) -> str:
    """
    Initiates an outbound call and returns the Conversation ID.
    """
    conversations_api = ConversationsApi(oauth_client=oauth_client)
    call_request = prepare_call_request(agent_id, to_number, from_number)

    try:
        # The API call
        response = conversations_api.post_conversations_calls(
            body=call_request
        )
        
        conversation_id = response.id
        print(f"Call initiated successfully. Conversation ID: {conversation_id}")
        
        # Optional: Return participant ID for further actions (e.g., hangup)
        # The response contains a 'participants' list.
        if response.participants:
            agent_participant_id = response.participants[0].id
            print(f"Agent Participant ID: {agent_participant_id}")
            
        return conversation_id
        
    except ApiException as e:
        print(f"Failed to initiate call: {e.status} {e.reason}")
        # Common errors:
        # 400: Invalid phone number format or invalid DID
        # 403: User does not have permission or is not logged in
        # 429: Rate limit exceeded
        raise

# Usage
# conversation_id = initiate_outbound_call(oauth_client, agent_id, "+15550000000", "+15559999999")

JavaScript Implementation

For Node.js environments, the async/await pattern simplifies error handling.

const { GenesysCloud } = require('@genesyscloud/genesyscloud');
const { ConversationsApi, ConversationCall } = require('@genesyscloud/genesyscloud/models');

async function initiateCallJS() {
    const genesys = new GenesysCloud({
        clientId: process.env.GENESYS_CLOUD_CLIENT_ID,
        clientSecret: process.env.GENESYS_CLOUD_CLIENT_SECRET,
        baseUri: process.env.GENESYS_CLOUD_BASE_URL || 'https://api.mypurecloud.com'
    });

    await genesys.authenticate();

    const conversationsApi = new ConversationsApi({
        authentication: genesys.authentication,
        baseUri: genesys.baseUri
    });

    const callBody = new ConversationCall({
        to: '+15550000000',      // Recipient
        from: '+15559999999',    // Caller ID (DID)
        userId: 'AGENT_USER_ID', // Replace with actual User ID
        type: 'voice'
    });

    try {
        const response = await conversationsApi.postConversationsCalls({
            body: callBody
        });

        console.log(`Call initiated. Conversation ID: ${response.id}`);
        return response.id;
    } catch (error) {
        console.error(`Error initiating call: ${error.status} ${error.message}`);
        throw error;
    }
}

// initiateCallJS();

C# Implementation

In .NET, the SDK uses synchronous and asynchronous methods. We use the async version for better performance.

using System;
using System.Threading.Tasks;
using GenesysCloud.Auth;
using GenesysCloud.Conversations;
using GenesysCloud.Models;

public class GenesysCaller
{
    private readonly IConversationsApi _conversationsApi;

    public GenesysCaller(IConversationsApi conversationsApi)
    {
        _conversationsApi = conversationsApi;
    }

    public async Task<string> InitiateCallAsync(string agentId, string toNumber, string fromNumber)
    {
        var callRequest = new ConversationCall
        {
            To = toNumber,
            From = fromNumber,
            UserId = agentId,
            Type = "voice"
        };

        try
        {
            var response = await _conversationsApi.PostConversationsCallsAsync(body: callRequest);
            Console.WriteLine($"Call initiated. Conversation ID: {response.Id}");
            return response.Id;
        }
        catch (ApiException ex)
        {
            Console.WriteLine($"Error: {ex.ErrorCode} {ex.Message}");
            throw;
        }
    }
}

// Usage Example:
// var auth = new OAuthClient(clientId, clientSecret, baseUrl);
// auth.Authenticate();
// var convApi = new ConversationsApi(auth);
// var caller = new GenesysCaller(convApi);
// await caller.InitiateCallAsync("AGENT_ID", "+15550000000", "+15559999999");

Complete Working Example

This Python script combines authentication, user lookup (optional, assuming ID is known for simplicity), and call initiation. It includes robust error handling and logging.

import os
import sys
import logging
from genesyscloud.auth import OAuthClient
from genesyscloud.conversations import ConversationsApi
from genesyscloud.conversations.models import ConversationCall
from genesyscloud.rest import ApiException

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def get_oauth_client() -> OAuthClient:
    client_id = os.environ.get("GENESYS_CLOUD_CLIENT_ID")
    client_secret = os.environ.get("GENESYS_CLOUD_CLIENT_SECRET")
    base_url = os.environ.get("GENESYS_CLOUD_BASE_URL", "https://api.mypurecloud.com")

    if not client_id or not client_secret:
        raise ValueError("Environment variables GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET are required.")

    oauth_client = OAuthClient(client_id=client_id, client_secret=client_secret, base_url=base_url)
    try:
        oauth_client.authenticate()
        return oauth_client
    except ApiException as e:
        logger.error(f"Authentication failed: {e.status} {e.reason}")
        sys.exit(1)

def make_outbound_call(oauth_client: OAuthClient, agent_id: str, to_number: str, from_number: str) -> str:
    """
    Initiates an outbound call.
    
    Args:
        oauth_client: Authenticated OAuth client.
        agent_id: The Genesys Cloud User ID of the agent.
        to_number: The recipient's phone number in E.164 format.
        from_number: The caller ID (DID) in E.164 format.
        
    Returns:
        The Conversation ID.
    """
    conversations_api = ConversationsApi(oauth_client=oauth_client)
    
    # Construct the request body
    call_request = ConversationCall(
        to=to_number,
        from_=from_number,
        user_id=agent_id,
        type="voice"
    )

    try:
        logger.info(f"Initiating call from {from_number} to {to_number} for agent {agent_id}")
        response = conversations_api.post_conversations_calls(body=call_request)
        
        conversation_id = response.id
        logger.info(f"Call initiated successfully. Conversation ID: {conversation_id}")
        
        # Extract participant ID for potential future actions (e.g., hangup)
        if response.participants:
            agent_participant_id = response.participants[0].id
            logger.info(f"Agent Participant ID: {agent_participant_id}")
            
        return conversation_id
        
    except ApiException as e:
        logger.error(f"API Error {e.status}: {e.reason}")
        
        if e.status == 400:
            logger.error("Bad Request. Check phone number formats (E.164) and DID validity.")
        elif e.status == 403:
            logger.error("Forbidden. Ensure the agent is logged in and has permission to make calls.")
        elif e.status == 404:
            logger.error("Not Found. The User ID or DID may be invalid.")
        elif e.status == 429:
            logger.error("Rate Limit Exceeded. Implement exponential backoff.")
            
        raise

def main():
    # Configuration
    AGENT_ID = os.environ.get("AGENT_USER_ID", "DEFAULT_AGENT_ID") # Replace with actual ID
    TO_NUMBER = os.environ.get("TO_NUMBER", "+15550000000")
    FROM_NUMBER = os.environ.get("FROM_NUMBER", "+15559999999")

    if not AGENT_ID:
        logger.error("AGENT_USER_ID environment variable is required.")
        sys.exit(1)

    try:
        oauth_client = get_oauth_client()
        conversation_id = make_outbound_call(oauth_client, AGENT_ID, TO_NUMBER, FROM_NUMBER)
        logger.info(f"Process complete. Conversation ID: {conversation_id}")
    except Exception as e:
        logger.error(f"Fatal error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 400 Bad Request

Cause: The most common cause is an invalid phone number format. Genesys Cloud requires E.164 format (e.g., +15551234567). Leading zeros, parentheses, or dashes often cause validation failures. Another cause is an invalid from number (DID) that is not provisioned in your Genesys Cloud organization.

Fix:

  1. Validate phone numbers using a library like phonenumbers in Python.
  2. Verify the from number exists in Setup > Calling > Phone numbers.
  3. Ensure the from number is associated with the agent or a team the agent belongs to.
# Python validation example
import phonenumbers

def validate_e164(phone_number: str) -> bool:
    try:
        parsed_number = phonenumbers.parse(phone_number, None)
        return phonenumbers.is_valid_number(parsed_number)
    except phonenumbers.NumberParseException:
        return False

Error: 403 Forbidden

Cause: The agent specified in userId is not currently logged into the Genesys Cloud Desktop application, or their presence is set to a state that does not allow outbound calls (e.g., “Offline”). Alternatively, the OAuth client lacks the conversation:call:write scope.

Fix:

  1. Ensure the agent is logged in to Genesys Cloud Desktop.
  2. Check the agent’s presence state. They should be “Available” or “Busy” (depending on org settings).
  3. Verify the OAuth client scopes in the Admin Console.

Error: 429 Too Many Requests

Cause: You have exceeded the API rate limits. Genesys Cloud enforces rate limits per client ID and per endpoint.

Fix: Implement exponential backoff and retry logic.

import time

def make_call_with_retry(oauth_client, agent_id, to_number, from_number, max_retries=3):
    for attempt in range(max_retries):
        try:
            return make_outbound_call(oauth_client, agent_id, to_number, from_number)
        except ApiException as e:
            if e.status == 429:
                wait_time = 2 ** attempt
                logger.warning(f"Rate limit hit. Retrying in {wait_time} seconds...")
                time.sleep(wait_time)
            else:
                raise
    raise Exception("Max retries exceeded for rate limiting.")

Error: 404 Not Found

Cause: The userId provided does not exist in your Genesys Cloud organization, or the from number (DID) is not found.

Fix: Double-check the User ID and DID values. Use the Admin Console to verify the exact IDs.

Official References