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
requestslibrary and the officialgenesyscloudPython 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:
genesyscloudPython 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 atoobject. In many SDK versions, you can pass the number directly in a simplified structure, but the raw API requires specific formatting.type: Must bepstn.
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.idis a valid UUID of a user in the organization. - Check that the
typeis set topstn.
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-Afterheader 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.