Making Outbound Calls on Behalf of Agents via API

Making Outbound Calls on Behalf of Agents via API

What You Will Build

  • You will write a script that initiates a PSTN outbound call from a Genesys Cloud user (agent) to an external number.
  • This tutorial uses the Genesys Cloud REST API endpoint POST /api/v2/conversations/calls.
  • The implementation covers Python using the requests library and the official genesyscloud Python SDK.

Prerequisites

  • OAuth Client Type: Confidential Client (Client Credentials Flow) or JWT Bearer Flow. Public clients cannot perform actions on behalf of other users.
  • Required Scopes:
    • conversation:call:write (Required to create the call).
    • user:read (Optional, if you need to look up user details dynamically).
    • user:read:me (Required if using JWT Bearer flow to generate the token for the specific agent).
  • SDK Version: genesyscloud Python SDK v2.0.0 or later.
  • Runtime: Python 3.8+.
  • Dependencies: requests, genesyscloud.

Authentication Setup

Genesys Cloud APIs require a valid access token. For outbound calls on behalf of an agent, the token must represent an identity that has permission to place calls.

If you use Client Credentials, the token represents the application. The POST /api/v2/conversations/calls endpoint requires you to explicitly specify the from user ID in the request body. The application must have the conversation:call:write scope.

If you use JWT Bearer, you generate a token for a specific user ID. The from field in the request body must match the user ID in the JWT. This is often preferred for “on behalf of” logic because permissions are tied to the user’s role.

Python: Getting a Token via Client Credentials

import requests
import base64
import json

def get_access_token(client_id: str, client_secret: str, org_id: str) -> str:
    """
    Retrieves an access token using Client Credentials Flow.
    """
    url = f"https://{org_id}.mygen.com/oauth/token"
    
    # Basic Auth header
    credentials = f"{client_id}:{client_secret}"
    encoded_credentials = base64.b64encode(credentials.encode()).decode()
    
    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": f"Basic {encoded_credentials}"
    }
    
    data = {
        "grant_type": "client_credentials",
        "scope": "conversation:call:write user:read"
    }
    
    response = requests.post(url, headers=headers, data=data)
    
    if response.status_code == 200:
        return response.json()["access_token"]
    else:
        raise Exception(f"Auth failed: {response.status_code} - {response.text}")

# Example usage
# token = get_access_token("your_client_id", "your_client_secret", "your_org_id")

Implementation

Step 1: Constructing the Call Request Payload

The core of this operation is the JSON payload sent to POST /api/v2/conversations/calls. You must define the from (the agent) and to (the destination) objects.

Key fields:

  • from.id: The UUID of the user placing the call.
  • from.name: The display name for the caller ID (optional but recommended).
  • from.phoneNumber: The PSTN number to use as the Caller ID. This must be a number owned by your Genesys Cloud organization and assigned to the user or available for use.
  • to.id: The target number. For PSTN, this is often just the phone number string, but strictly speaking, the API expects a to object. In many SDK versions, you can pass the number directly in a simplified structure, but the raw API requires specific formatting.
  • type: Must be pstn.

Raw API Payload Example

{
  "from": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "John Doe",
    "phoneNumber": "+15551234567"
  },
  "to": {
    "phoneNumber": "+15559876543"
  },
  "type": "pstn"
}

Note on Caller ID: The phoneNumber in the from object must match a number provisioned in your Genesys Cloud account. If you use a number not assigned to the user or not owned by the org, the call will fail with a 400 Bad Request or 403 Forbidden.

Step 2: Making the Call Using the Python SDK

The official SDK simplifies the request by handling serialization and error parsing.

from genesyscloud import ApiClient, Configuration
from genesyscloud.rest import ApiException
from genesyscloud.conversations_api import ConversationsApi
import os

def make_outbound_call_sdk(
    org_id: str, 
    client_id: str, 
    client_secret: str,
    from_user_id: str,
    from_phone_number: str,
    to_phone_number: str
):
    """
    Initiates an outbound PSTN call using the Genesys Cloud Python SDK.
    """
    # 1. Configure the client
    configuration = Configuration()
    configuration.host = f"https://{org_id}.mygen.com/api/v2"
    
    # Use OAuth client credentials
    api_client = ApiClient(configuration)
    api_client.refresh_token_credentials(client_id, client_secret)
    
    # 2. Initialize the Conversations API client
    conversations_api = ConversationsApi(api_client)
    
    # 3. Build the request body
    # The SDK expects a ConversationsCallsPostRequest object
    from genesyscloud.models import ConversationsCallsPostRequest, ConversationsCallsPostFrom, ConversationsCallsPostTo
    
    # Define the 'from' object
    from_obj = ConversationsCallsPostFrom(
        id=from_user_id,
        phone_number=from_phone_number
    )
    
    # Define the 'to' object
    to_obj = ConversationsCallsPostTo(
        phone_number=to_phone_number
    )
    
    # Construct the full request
    call_request = ConversationsCallsPostRequest(
        from_=from_obj,
        to=to_obj,
        type="pstn"
    )
    
    try:
        # 4. Execute the call
        response = conversations_api.post_conversations_calls(body=call_request)
        
        print(f"Call Initiated Successfully.")
        print(f"Conversation ID: {response.id}")
        print(f"Status: {response.status}")
        
        return response.id
        
    except ApiException as e:
        print(f"Exception when calling ConversationsApi->post_conversations_calls: {e}")
        if e.status == 400:
            print("Bad Request: Check phone number formats or user ID.")
        elif e.status == 403:
            print("Forbidden: Check user permissions or Caller ID ownership.")
        elif e.status == 429:
            print("Rate Limited: Implement retry logic.")
        raise

# Example Usage
# conversation_id = make_outbound_call_sdk(
#     org_id="your_org_id",
#     client_id="your_client_id",
#     client_secret="your_client_secret",
#     from_user_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
#     from_phone_number="+15551234567",
#     to_phone_number="+15559876543"
# )

Step 3: Handling the Response and Conversation State

The POST request returns immediately with a 201 Created status. It does not wait for the call to connect. The response body contains the Conversation object, including the unique conversationId.

You must monitor the conversation state asynchronously if you need to know when the call connects, rings, or fails.

Polling for Conversation Status

import time

def monitor_call_status(
    api_client: ApiClient,
    conversation_id: str,
    max_wait_seconds: int = 60
):
    """
    Polls the conversation status until it changes from 'queued' or 'initiated'.
    """
    conversations_api = ConversationsApi(api_client)
    start_time = time.time()
    
    while time.time() - start_time < max_wait_seconds:
        try:
            # Get the conversation details
            conv = conversations_api.get_conversation(conversation_id)
            
            print(f"Current Status: {conv.status}")
            
            # If the call is no longer in an initial state, we can stop polling
            if conv.status not in ["queued", "initiated"]:
                print(f"Call transitioned to: {conv.status}")
                return conv.status
                
        except ApiException as e:
            print(f"Error fetching conversation status: {e}")
            break
            
        # Wait before next poll to avoid rate limiting
        time.sleep(2)
        
    print("Timed out waiting for call status update.")
    return None

Complete Working Example

This script combines authentication, call initiation, and basic status monitoring. It uses the requests library for lower-level control, demonstrating the raw HTTP interaction.

import requests
import base64
import time
import json

class GenesysOutboundCaller:
    def __init__(self, org_id: str, client_id: str, client_secret: str):
        self.org_id = org_id
        self.client_id = client_id
        self.client_secret = client_secret
        self.base_url = f"https://{org_id}.mygen.com/api/v2"
        self.token_url = f"https://{org_id}.mygen.com/oauth/token"
        self.access_token = None

    def authenticate(self):
        """Obtain OAuth2 Token"""
        credentials = f"{self.client_id}:{self.client_secret}"
        encoded_credentials = base64.b64encode(credentials.encode()).decode()
        
        headers = {
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": f"Basic {encoded_credentials}"
        }
        
        data = {
            "grant_type": "client_credentials",
            "scope": "conversation:call:write"
        }
        
        response = requests.post(self.token_url, headers=headers, data=data)
        
        if response.status_code == 200:
            self.access_token = response.json()["access_token"]
            return True
        else:
            raise Exception(f"Authentication failed: {response.text}")

    def make_call(self, from_user_id: str, from_number: str, to_number: str) -> str:
        """
        Initiates an outbound call.
        Returns the Conversation ID.
        """
        if not self.access_token:
            self.authenticate()

        url = f"{self.base_url}/conversations/calls"
        
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.access_token}"
        }
        
        payload = {
            "from": {
                "id": from_user_id,
                "phoneNumber": from_number
            },
            "to": {
                "phoneNumber": to_number
            },
            "type": "pstn"
        }
        
        response = requests.post(url, headers=headers, json=payload)
        
        if response.status_code == 201:
            data = response.json()
            print(f"Call initiated. Conversation ID: {data['id']}")
            return data['id']
        else:
            raise Exception(f"Failed to initiate call: {response.status_code} - {response.text}")

    def get_call_status(self, conversation_id: str) -> dict:
        """
        Retrieves the current status of the conversation.
        """
        if not self.access_token:
            self.authenticate()

        url = f"{self.base_url}/conversations/{conversation_id}"
        headers = {
            "Authorization": f"Bearer {self.access_token}"
        }
        
        response = requests.get(url, headers=headers)
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"Failed to get status: {response.text}")

# --- Execution Block ---
if __name__ == "__main__":
    # Configuration
    ORG_ID = "your_org_id"
    CLIENT_ID = "your_client_id"
    CLIENT_SECRET = "your_client_secret"
    
    # Call Details
    FROM_USER_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" # The Agent's User ID
    FROM_NUMBER = "+15551234567" # The Caller ID Number
    TO_NUMBER = "+15559876543"   # The Destination Number

    try:
        caller = GenesysOutboundCaller(ORG_ID, CLIENT_ID, CLIENT_SECRET)
        
        # 1. Initiate Call
        conv_id = caller.make_call(FROM_USER_ID, FROM_NUMBER, TO_NUMBER)
        
        # 2. Monitor Status (Simple Polling)
        print("Monitoring call status...")
        for _ in range(10):
            status_data = caller.get_call_status(conv_id)
            status = status_data.get("status")
            print(f"Status: {status}")
            
            # Stop if the call is no longer ringing/queued
            if status in ["connected", "disconnected", "failed"]:
                break
                
            time.sleep(2)
            
    except Exception as e:
        print(f"Error: {e}")

Common Errors & Debugging

Error: 400 Bad Request

Cause: The phone number format is invalid, or the from user ID does not exist.
Fix:

  • Ensure phone numbers are in E.164 format (e.g., +15551234567).
  • Verify the from.id is a valid UUID of a user in the organization.
  • Check that the type is set to pstn.

Error: 403 Forbidden

Cause: The Caller ID (from.phoneNumber) is not owned by the organization or not assigned to the user.
Fix:

  • Go to the Genesys Cloud Admin Console > Numbers & Addresses.
  • Ensure the number is “Provisioned” and “Active”.
  • Ensure the number is associated with the User ID specified in from.id, or that the user has a role that allows using that number.

Error: 429 Too Many Requests

Cause: You have exceeded the rate limit for the POST /api/v2/conversations/calls endpoint.
Fix:

  • Implement exponential backoff.
  • Check the Retry-After header in the response.
  • Cache tokens to avoid excessive authentication requests.

Error: 500 Internal Server Error

Cause: Temporary service issue or invalid state in the Genesys Cloud backend.
Fix:

  • Wait and retry.
  • Check the Genesys Cloud Status Page for outages.

Official References