Programmatically Initiate Outbound Calls on Behalf of Agents in Genesys Cloud CX
What You Will Build
- A Python script that programmatically initiates an outbound voice call from a specific Genesys Cloud user (agent) to an external phone number.
- This tutorial uses the Genesys Cloud CX REST API endpoint
POST /api/v2/conversations/calls. - The implementation covers OAuth2 client credential flow, payload construction, and robust error handling for 4xx and 5xx responses.
Prerequisites
OAuth Client Configuration
To execute this API call, you require a Genesys Cloud OAuth Client with the following configuration:
- Client Type: Confidential Client (Client Credentials Grant).
- Required Scopes:
conversation:call:write(Required to create the call resource).user:read(Optional, but recommended if you need to validate user existence before calling).
Environment Requirements
- Language: Python 3.8+
- Dependencies:
requests(v2.28.0+) for HTTP communication.pyjwt(optional, for debugging token structures, though not strictly needed for this tutorial).
Install dependencies via pip:
pip install requests
Account Permissions
The user associated with the OAuth client must have the Application Administrator or Call Center Administrator role, or specific custom permissions allowing programmatic call creation. Additionally, the userId provided in the payload must belong to a user who is enabled for voice interactions and has a valid routing profile.
Authentication Setup
Genesys Cloud CX uses OAuth 2.0 for authentication. For server-to-server integrations, the Client Credentials Grant is the standard flow. This flow exchanges your client ID and client secret for an access token.
Step 1: Obtain an Access Token
The token endpoint is https://api.mypurecloud.com/oauth/token. You must send a POST request with the grant type, client ID, and client secret.
import requests
import os
from typing import Dict, Optional
class GenesysAuth:
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.token_url = f"https://api.mypurecloud.com/oauth/token"
self.api_base_url = f"https://api.mypurecloud.com/api/v2"
self.access_token: Optional[str] = None
def get_access_token(self) -> str:
"""
Retrieves an OAuth2 access token using the Client Credentials Grant.
"""
if self.access_token:
# In a production environment, implement token expiration checking here.
# Tokens typically expire after 3600 seconds.
return self.access_token
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(self.token_url, headers=headers, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
return self.access_token
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("Authentication failed: Invalid Client ID or Secret.") from e
raise Exception(f"Failed to obtain token: {response.text}") from e
except requests.exceptions.RequestException as e:
raise Exception(f"Network error during authentication: {e}") from e
Important Note on Token Caching:
The code above includes a basic check for an existing token. In a long-running service, you must track the expires_in field returned by the OAuth endpoint and refresh the token before it expires to avoid 401 Unauthorized errors during API calls.
Implementation
Step 1: Construct the Call Payload
The POST /api/v2/conversations/calls endpoint requires a JSON body that defines the originator, the destination, and the type of interaction.
Key fields in the payload:
originator: An object containing theuserId(the Genesys Cloud user ID making the call) andexternalContact(optional, but good for logging).to: The destination phone number in E.164 format (e.g.,+14155551234).from: The outbound phone number associated with your Genesys Cloud account that will appear as the caller ID.type: Must be"outbound"for this use case.wrapUpCode: Optional. If provided, the call will automatically wrap up with this code when the agent ends the call.
def build_call_payload(user_id: str, from_number: str, to_number: str) -> Dict:
"""
Constructs the JSON payload for initiating an outbound call.
Args:
user_id: The UUID of the Genesys Cloud user initiating the call.
from_number: The E.164 formatted outbound phone number owned by the org.
to_number: The E.164 formatted destination phone number.
Returns:
A dictionary representing the JSON payload.
"""
payload = {
"originator": {
"userId": user_id,
"externalContact": {
"id": "system-generated",
"name": "Programmatic Outbound Call"
}
},
"to": to_number,
"from": from_number,
"type": "outbound",
"skillIds": [], # Optional: Add skill IDs if routing logic requires specific skills
"wrapUpCode": "completed" # Optional: Default wrap-up code
}
return payload
Step 2: Execute the API Call
With the token and payload ready, you send a POST request to https://api.mypurecloud.com/api/v2/conversations/calls.
import json
class GenesysCallManager:
def __init__(self, auth: GenesysAuth):
self.auth = auth
self.api_endpoint = f"{auth.api_base_url}/conversations/calls"
def initiate_outbound_call(self, user_id: str, from_number: str, to_number: str) -> Dict:
"""
Initiates an outbound call on behalf of a user.
Args:
user_id: The UUID of the user making the call.
from_number: The outbound phone number (E.164).
to_number: The destination phone number (E.164).
Returns:
The response JSON from the API, including the conversation ID.
"""
token = self.auth.get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = build_call_payload(user_id, from_number, to_number)
try:
response = requests.post(
self.api_endpoint,
headers=headers,
json=payload
)
# Raise an exception for 4xx and 5xx status codes
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
self._handle_http_error(response, e)
except requests.exceptions.RequestException as e:
raise Exception(f"Network error while initiating call: {e}") from e
def _handle_http_error(self, response: requests.Response, error: Exception) -> None:
"""
Handles specific HTTP error codes and provides actionable feedback.
"""
status_code = response.status_code
error_body = response.json() if response.text else {}
if status_code == 401:
raise Exception("Unauthorized: Access token is invalid or expired.") from error
elif status_code == 403:
raise Exception(f"Forbidden: Insufficient permissions. Check scopes and user roles. Details: {error_body}") from error
elif status_code == 404:
raise Exception(f"Not Found: The user ID or phone number may be invalid. Details: {error_body}") from error
elif status_code == 429:
raise Exception("Rate Limited: Too many requests. Implement exponential backoff.") from error
elif status_code == 500:
raise Exception("Internal Server Error: Genesys Cloud encountered an unexpected error.") from error
else:
raise Exception(f"HTTP Error {status_code}: {error_body}") from error
Step 3: Processing Results
Upon success, the API returns a 201 Created status with the full conversation object. The most critical value to extract is the id field, which is the Conversation ID. You need this ID for subsequent operations, such as:
- Updating the call status.
- Retrieving conversation analytics.
- Ending the call programmatically.
Expected Successful Response (201 Created):
{
"id": "12345678-1234-1234-1234-123456789012",
"type": "voice",
"state": "connected",
"direction": "outbound",
"originator": {
"userId": "87654321-4321-4321-4321-210987654321",
"externalContact": {
"id": "system-generated",
"name": "Programmatic Outbound Call"
}
},
"to": "+14155551234",
"from": "+16505559876",
"startTime": "2023-10-27T14:30:00.000Z",
"answeredTime": "2023-10-27T14:30:05.000Z",
"wrapUpCode": "completed",
"skillIds": [],
"monitoring": {
"isMonitored": false
}
}
Complete Working Example
Below is a complete, runnable Python script. Replace the placeholder values with your actual Genesys Cloud credentials.
import os
import sys
import requests
from typing import Dict, Optional
# ==========================================
# Configuration
# ==========================================
GENESYS_ORG_ID = "your-org-id"
GENESYS_CLIENT_ID = "your-client-id"
GENESYS_CLIENT_SECRET = "your-client-secret"
GENESYS_USER_ID = "agent-user-uuid-here" # The user who will appear as the caller
GENESYS_FROM_NUMBER = "+16505559876" # Your outbound DID
GENESYS_TO_NUMBER = "+14155551234" # Destination number
# ==========================================
# Authentication Module
# ==========================================
class GenesysAuth:
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.token_url = f"https://api.mypurecloud.com/oauth/token"
self.api_base_url = f"https://api.mypurecloud.com/api/v2"
self.access_token: Optional[str] = None
def get_access_token(self) -> str:
if self.access_token:
return self.access_token
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(self.token_url, headers=headers, data=data)
response.raise_for_status()
self.access_token = response.json()["access_token"]
return self.access_token
except requests.exceptions.RequestException as e:
raise Exception(f"Authentication failed: {e}") from e
# ==========================================
# Call Management Module
# ==========================================
def build_call_payload(user_id: str, from_number: str, to_number: str) -> Dict:
return {
"originator": {
"userId": user_id,
"externalContact": {
"id": "automation-script",
"name": "Automated Outbound"
}
},
"to": to_number,
"from": from_number,
"type": "outbound",
"wrapUpCode": "completed"
}
def initiate_call(auth: GenesysAuth, user_id: str, from_num: str, to_num: str):
token = auth.get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = build_call_payload(user_id, from_num, to_num)
endpoint = f"{auth.api_base_url}/conversations/calls"
print(f"Initiating call from {from_num} to {to_num} as user {user_id}...")
try:
response = requests.post(endpoint, headers=headers, json=payload)
if response.status_code == 201:
result = response.json()
print(f"SUCCESS: Call initiated.")
print(f"Conversation ID: {result['id']}")
print(f"State: {result['state']}")
return result['id']
else:
print(f"ERROR: HTTP {response.status_code}")
print(f"Response: {response.text}")
return None
except Exception as e:
print(f"Exception occurred: {e}")
return None
# ==========================================
# Main Execution
# ==========================================
if __name__ == "__main__":
# Validate environment variables or use hardcoded values for demo
if not GENESYS_CLIENT_ID or GENESYS_CLIENT_ID == "your-client-id":
print("ERROR: Please configure your Genesys Cloud credentials in the script.")
sys.exit(1)
try:
auth = GenesysAuth(GENESYS_ORG_ID, GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET)
conversation_id = initiate_call(
auth=auth,
user_id=GENESYS_USER_ID,
from_num=GENESYS_FROM_NUMBER,
to_num=GENESYS_TO_NUMBER
)
if conversation_id:
print(f"\nYou can track this conversation using ID: {conversation_id}")
print(f"URL: https://admin.mypurecloud.com/conversations/{conversation_id}")
except Exception as e:
print(f"Fatal Error: {e}")
sys.exit(1)
Common Errors & Debugging
Error: 403 Forbidden - “User does not have permission”
Cause: The OAuth client lacks the conversation:call:write scope, or the user specified in originator.userId does not have the “Make Calls” permission in their Role.
Fix:
- Go to Setup > Apps and Integrations > OAuth Management.
- Edit your client and ensure
conversation:call:writeis checked. - Verify the target user has a Role with “Voice” permissions enabled.
Error: 400 Bad Request - “Invalid phone number format”
Cause: The from or to fields are not in strict E.164 format.
Fix: Ensure numbers start with + followed by the country code, with no spaces, dashes, or parentheses.
- Invalid:
(415) 555-1234 - Valid:
+14155551234
Error: 404 Not Found - “User not found”
Cause: The userId in the originator object does not exist in your Genesys Cloud organization.
Fix: Verify the UUID. You can find user IDs in the Genesys Cloud Admin console by hovering over the user’s name, or by querying GET /api/v2/users/me if authenticating as that user.
Error: 429 Too Many Requests
Cause: You have exceeded the rate limit for call creation (typically 100 calls per minute per client, though this varies by plan).
Fix: Implement exponential backoff. If you receive a 429, wait for the duration specified in the Retry-After header before retrying.
# Example retry logic snippet
import time
def post_with_retry(url, headers, json_data, max_retries=3):
for attempt in range(max_retries):
response = requests.post(url, headers=headers, json=json_data)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
print(f"Rate limited. Retrying in {retry_after} seconds...")
time.sleep(retry_after)
continue
return response
return response