Initiating an Outbound Call on Behalf of an Agent Using Genesys Cloud API

Initiating an Outbound Call on Behalf of an Agent Using Genesys Cloud API

What You Will Build

  • This tutorial demonstrates how to programmatically trigger an outbound voice call from your Genesys Cloud CX organization to an external telephone number.
  • The implementation utilizes the Genesys Cloud REST API endpoint POST /api/v2/conversations/calls via the official Python SDK.
  • The code is written in Python 3.9+ and handles authentication, payload construction, and error recovery.

Prerequisites

OAuth Client Configuration

You must create a Client ID and Client Secret in the Genesys Cloud Admin Portal.

  1. Navigate to Admin > Security > API Access.
  2. Create a new client.
  3. Assign the OAuth Grant Type as Client Credentials (for service-to-service automation) or Authorization Code (if acting on behalf of a logged-in user with a refresh token). For this tutorial, we assume a service account using Client Credentials.
  4. Assign the following OAuth Scopes to the client:
    • conversation:call:write (Required to create the conversation)
    • conversation:call (Required to read conversation details)
    • user:read (Optional, if you need to resolve agent names)

Environment Setup

  • Python: Version 3.9 or higher.
  • Package Manager: pip.
  • SDK: genesyscloud (The official Python SDK).

Install the SDK via terminal:

pip install genesyscloud

Authentication Setup

Genesys Cloud uses OAuth 2.0 for authentication. The Python SDK handles the token acquisition and refreshing automatically when initialized correctly. You must provide your Organization ID, Client ID, and Client Secret.

Never hardcode credentials in production code. Use environment variables.

import os
from purecloudplatformclientv2 import Configuration, ApiClient, ConversationApi

def get_authed_api_client():
    """
    Initializes and returns an authenticated ConversationApi client.
    """
    # Retrieve credentials from environment variables
    org_id = os.getenv("GENESYS_ORG_ID")
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")

    if not all([org_id, client_id, client_secret]):
        raise ValueError("Missing required environment variables: GENESYS_ORG_ID, GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET")

    # Configure the SDK
    configuration = Configuration(
        host="https://api.mypurecloud.com",
        org_id=org_id,
        client_id=client_id,
        client_secret=client_secret
    )

    # Create the API client
    api_client = ApiClient(configuration=configuration)

    # Initialize the specific API module
    conversation_api = ConversationApi(api_client)

    return conversation_api

The SDK caches the access token. When the token expires, the SDK automatically requests a new one using the stored client credentials. This logic is transparent to your application code.

Implementation

Step 1: Constructing the Outbound Call Payload

The core of an outbound call is the OutboundCall request body. This object defines who is calling, who is being called, and how the call is routed.

Key fields in the payload:

  • from: The Genesys Cloud user ID initiating the call. This user must have a valid phone number assigned.
  • to: The destination telephone number in E.164 format (e.g., +14155551234).
  • route: How the call connects. The most common value is user, which routes the call directly to the specified user’s device.
  • wrapUpCode: Optional. If provided, the call is automatically wrapped up with this code upon completion.
from purecloudplatformclientv2 import OutboundCall

def create_outbound_call_payload(agent_id: str, destination_number: str, wrap_up_code: str = None) -> OutboundCall:
    """
    Constructs the OutboundCall object for the API request.
    
    Args:
        agent_id (str): The UUID of the Genesys Cloud user making the call.
        destination_number (str): The E.164 formatted phone number to call.
        wrap_up_code (str, optional): The ID of the wrap-up code to apply on hangup.
    
    Returns:
        OutboundCall: The configured request body.
    """
    # Initialize the outbound call object
    outbound_call = OutboundCall()
    
    # Set the caller (the agent)
    outbound_call.from_ = agent_id
    
    # Set the callee (the external number)
    outbound_call.to = destination_number
    
    # Set the routing strategy
    # 'user' means the call rings the specific user's devices configured in their profile
    outbound_call.route = "user"
    
    # Optional: Set a wrap-up code if required by your business process
    if wrap_up_code:
        outbound_call.wrap_up_code = wrap_up_code
        
    return outbound_call

Note on the from_ field: In Python, from is a reserved keyword. The SDK maps the JSON field from to the attribute from_. Ensure you use from_ when setting the attribute.

Step 2: Executing the API Call

Once the payload is constructed, you send it to the POST /api/v2/conversations/calls endpoint. The API returns a Conversation object containing the unique conversation ID. This ID is critical for tracking the call status, transferring the call, or merging it later.

from purecloudplatformclientv2.rest import ApiException
import logging

logger = logging.getLogger(__name__)

def initiate_call(conversation_api, payload: OutboundCall) -> str:
    """
    Sends the outbound call request to Genesys Cloud.
    
    Args:
        conversation_api: The authenticated ConversationApi client.
        payload (OutboundCall): The request body.
    
    Returns:
        str: The conversation ID of the initiated call.
    
    Raises:
        ApiException: If the API request fails.
    """
    try:
        # Execute the POST request
        # The API returns a Conversation object
        response = conversation_api.post_conversations_calls(body=payload)
        
        conversation_id = response.id
        logger.info(f"Call initiated successfully. Conversation ID: {conversation_id}")
        
        return conversation_id
        
    except ApiException as e:
        # Handle specific HTTP errors
        if e.status == 401:
            logger.error("Authentication failed. Check Client ID and Secret.")
        elif e.status == 403:
            logger.error("Forbidden. Check OAuth scopes. Required: conversation:call:write")
        elif e.status == 400:
            logger.error(f"Bad Request. Invalid payload: {e.body}")
        else:
            logger.error(f"API Error {e.status}: {e.reason}")
        raise

Step 3: Handling Asynchronous Call Status

The POST request returns immediately, often before the callee answers. The conversation state transitions through several phases:

  1. initiated: The call is being set up.
  2. alerting: The callee’s phone is ringing.
  3. connected: The callee has answered.
  4. wrapping: The call has ended, and the agent is wrapping up.
  5. closed: The conversation is fully closed.

If you need to act when the call connects (e.g., log the event, send a webhook, or transfer), you must poll the conversation status or use the Conversations Streaming API. For simplicity, this tutorial shows a polling mechanism to wait for the connected state.

import time
from purecloudplatformclientv2 import Conversation

def wait_for_connection(conversation_api, conversation_id: str, timeout_seconds: int = 30) -> Conversation:
    """
    Polls the conversation status until it is connected or timeout is reached.
    
    Args:
        conversation_api: The authenticated ConversationApi client.
        conversation_id (str): The ID of the conversation to monitor.
        timeout_seconds (int): Maximum time to wait for connection.
    
    Returns:
        Conversation: The final conversation object.
    
    Raises:
        TimeoutError: If the call does not connect within the timeout period.
    """
    start_time = time.time()
    
    while time.time() - start_time < timeout_seconds:
        try:
            # Get the current conversation details
            response = conversation_api.get_conversations_call_by_id(conversation_id)
            
            current_state = response.state
            
            if current_state == "connected":
                logger.info(f"Call connected to {response.to}")
                return response
            elif current_state in ["closed", "failed"]:
                logger.warning(f"Call ended in state: {current_state}")
                return response
            
            # Wait before polling again to respect rate limits
            time.sleep(2)
            
        except ApiException as e:
            logger.error(f"Error polling conversation: {e.reason}")
            raise

    raise TimeoutError(f"Call did not connect within {timeout_seconds} seconds.")

Complete Working Example

This script combines all components into a single executable module. It reads credentials from environment variables, initiates a call, and waits for the connection status.

import os
import sys
import logging
import time
from purecloudplatformclientv2 import (
    Configuration, 
    ApiClient, 
    ConversationApi, 
    OutboundCall,
    ApiException
)

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

def get_authed_api_client():
    """
    Initializes and returns an authenticated ConversationApi client.
    """
    org_id = os.getenv("GENESYS_ORG_ID")
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")

    if not all([org_id, client_id, client_secret]):
        raise ValueError("Missing required environment variables: GENESYS_ORG_ID, GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET")

    configuration = Configuration(
        host="https://api.mypurecloud.com",
        org_id=org_id,
        client_id=client_id,
        client_secret=client_secret
    )

    api_client = ApiClient(configuration=configuration)
    conversation_api = ConversationApi(api_client)

    return conversation_api

def create_outbound_call_payload(agent_id: str, destination_number: str) -> OutboundCall:
    """
    Constructs the OutboundCall object.
    """
    outbound_call = OutboundCall()
    outbound_call.from_ = agent_id
    outbound_call.to = destination_number
    outbound_call.route = "user"
    return outbound_call

def initiate_call(conversation_api, payload: OutboundCall) -> str:
    """
    Sends the outbound call request.
    """
    try:
        response = conversation_api.post_conversations_calls(body=payload)
        logger.info(f"Call initiated. Conversation ID: {response.id}")
        return response.id
    except ApiException as e:
        logger.error(f"Failed to initiate call: {e.reason}")
        raise

def wait_for_connection(conversation_api, conversation_id: str, timeout_seconds: int = 30):
    """
    Polls for connection status.
    """
    start_time = time.time()
    while time.time() - start_time < timeout_seconds:
        try:
            response = conversation_api.get_conversations_call_by_id(conversation_id)
            if response.state == "connected":
                logger.info("Call connected successfully.")
                return True
            elif response.state in ["closed", "failed"]:
                logger.warning(f"Call failed or closed with state: {response.state}")
                return False
            time.sleep(2)
        except ApiException as e:
            logger.error(f"Polling error: {e.reason}")
            return False
    logger.warning("Timeout waiting for connection.")
    return False

def main():
    # Configuration from environment or hardcoded for testing
    AGENT_ID = os.getenv("AGENT_ID", "replace-with-user-uuid")
    DESTINATION_NUMBER = os.getenv("DEST_NUMBER", "+14155551234")
    
    if AGENT_ID == "replace-with-user-uuid":
        logger.error("Please set AGENT_ID environment variable to a valid User UUID.")
        sys.exit(1)

    try:
        # 1. Authenticate
        logger.info("Authenticating with Genesys Cloud...")
        conversation_api = get_authed_api_client()
        
        # 2. Build Payload
        logger.info(f"Preparing call from Agent {AGENT_ID} to {DESTINATION_NUMBER}")
        payload = create_outbound_call_payload(AGENT_ID, DESTINATION_NUMBER)
        
        # 3. Initiate Call
        conversation_id = initiate_call(conversation_api, payload)
        
        # 4. Wait for Connection
        logger.info(f"Monitoring conversation {conversation_id}...")
        is_connected = wait_for_connection(conversation_api, conversation_id)
        
        if is_connected:
            logger.info("Outbound call completed successfully.")
        else:
            logger.info("Outbound call did not connect or failed.")
            
    except Exception as e:
        logger.error(f"An unexpected error occurred: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 403 Forbidden

Cause: The OAuth client lacks the necessary scope.
Fix: Ensure the client has the conversation:call:write scope. In the Admin Portal, go to Security > API Access, edit the client, and add the scope. Save and re-authenticate.

Error: 400 Bad Request - Invalid From User

Cause: The from_ user ID provided is invalid, does not exist, or does not have a phone number assigned.
Fix: Verify the AGENT_ID is a valid UUID of a Genesys Cloud user. Ensure the user has a “Phone” device assigned in their profile under Admin > Users.

Error: 429 Too Many Requests

Cause: You have exceeded the API rate limit for the POST /api/v2/conversations/calls endpoint.
Fix: Implement exponential backoff. Do not retry immediately. Wait 1 second, then 2, then 4, etc. The SDK does not automatically retry 429s for write operations to prevent duplicate calls.

import time

def retry_with_backoff(func, *args, max_retries=3, **kwargs):
    for attempt in range(max_retries):
        try:
            return func(*args, **kwargs)
        except ApiException as e:
            if e.status == 429:
                wait_time = 2 ** attempt
                logger.warning(f"Rate limited. Retrying in {wait_time} seconds...")
                time.sleep(wait_time)
            else:
                raise
    raise Exception("Max retries exceeded")

Error: Call Does Not Ring

Cause: The route parameter is set incorrectly, or the user is set to “Do Not Disturb” (DND).
Fix: Check the user’s DND status via the API (GET /api/v2/users/{userId}/presence). If DND is active, the call will not ring. Ensure route is set to user for direct agent calls.

Official References