Programmatic Call Transfer via the Genesys Cloud Conversations API
What You Will Build
- This tutorial demonstrates how to programmatically transfer an active voice conversation from one queue to another using the Genesys Cloud Conversations API.
- It utilizes the
PATCH /api/v2/conversations/voice/{conversationId}endpoint with thetransferaction. - The implementation is provided in Python using the official
genesyscloudSDK and rawrequestsfor clarity on payload structure.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Grant).
- Required Scopes:
conversation:transfer:write(To perform the transfer)conversation:view(To read conversation details and status)queue:member:view(Optional, if you need to validate queue membership beforehand)
- SDK Version:
genesyscloud>= 1.0.0 (Python) or@genesyscloud/genesyscloud(Node.js). - Runtime: Python 3.8+.
- Dependencies:
pip install genesyscloud requests
Authentication Setup
Before interacting with the Conversations API, you must obtain a valid access token. The Genesys Cloud API uses OAuth 2.0 Client Credentials flow for server-to-server integrations.
The following Python function handles token acquisition and caching. In production, implement a TTL (Time-To-Live) check to refresh the token before it expires (typically 3600 seconds).
import requests
import time
from typing import Optional
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, environment: str = "my.genesys.cloud"):
self.client_id = client_id
self.client_secret = client_secret
self.environment = environment
self.token_url = f"https://{environment}/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0.0
def get_access_token(self) -> str:
"""
Retrieves a new access token if the current one is expired or missing.
"""
if self.access_token and time.time() < self.token_expiry:
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
}
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"]
# Set expiry slightly before actual expiry to avoid race conditions
self.token_expiry = time.time() + token_data["expires_in"] - 60
return self.access_token
# Initialize with your credentials
# auth = GenesysAuth("your_client_id", "your_client_secret")
# token = auth.get_access_token()
Implementation
Step 1: Identify the Conversation and Target Queue
To transfer a call, you need two distinct identifiers:
- Conversation ID: The UUID of the active voice conversation.
- Target Queue ID: The UUID of the queue you want to transfer the call to.
You cannot transfer a call that is not in the queued, waiting, or connected state. Additionally, the system user performing the transfer must have the conversation:transfer:write permission.
If you do not have the Queue ID, you can retrieve it by listing queues or searching by name. Here is how to find a queue by name using the Search API, which is often more reliable than iterating through all queues.
import requests
def find_queue_by_name(auth: GenesysAuth, queue_name: str) -> Optional[str]:
"""
Searches for a queue by name and returns its ID.
"""
token = auth.get_access_token()
# Using the Search API to find queues by name
url = f"https://{auth.environment}/api/v2/search"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# Query for entity type 'queue' and match name
payload = {
"query": {
"bool": {
"must": [
{
"term": {
"entityType": "queue"
}
},
{
"match": {
"name": queue_name
}
}
]
}
}
}
response = requests.post(url, json=payload, headers=headers)
if response.status_code == 200:
results = response.json().get("results", [])
if results:
# Return the ID of the first match
return results[0].get("id")
return None
# Example usage:
# queue_id = find_queue_by_name(auth, "Support Tier 2")
# if not queue_id:
# raise ValueError("Queue not found")
Step 2: Construct the Transfer Payload
The core of the transfer operation is the PATCH request to /api/v2/conversations/voice/{conversationId}.
The body of this request must contain an array of actions. For a queue transfer, the action object requires:
- action: The string literal
"transfer". - toType: The string literal
"queue". - toId: The UUID of the target queue.
Optional but recommended fields:
- reasonCode: A reason code object if your organization requires transfer reasons.
- wrapUpCode: If you are transferring a connected agent, you might need to specify how the current leg should close.
Below is the structure of the JSON payload. Note that the toId must be a valid Queue ID.
{
"actions": [
{
"action": "transfer",
"toType": "queue",
"toId": "00000000-0000-0000-0000-000000000000"
}
]
}
Step 3: Execute the Transfer
The following Python function performs the actual transfer. It includes error handling for common status codes such as 404 (Conversation not found), 403 (Insufficient permissions), and 409 (Conversation state does not allow transfer).
import requests
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def transfer_call_to_queue(
auth: GenesysAuth,
conversation_id: str,
target_queue_id: str,
environment: str = "my.genesys.cloud"
) -> dict:
"""
Transfers an active voice conversation to a specified queue.
Args:
auth: GenesysAuth instance with valid token.
conversation_id: UUID of the active conversation.
target_queue_id: UUID of the destination queue.
environment: The Genesys Cloud environment URL.
Returns:
The JSON response from the API.
Raises:
requests.exceptions.HTTPError: If the API returns a non-2xx status.
"""
token = auth.get_access_token()
url = f"https://{environment}/api/v2/conversations/voice/{conversation_id}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = {
"actions": [
{
"action": "transfer",
"toType": "queue",
"toId": target_queue_id
}
]
}
try:
response = requests.patch(url, json=payload, headers=headers)
# Check for success. Note: PATCH often returns 200 or 204.
if response.status_code == 204:
logger.info(f"Successfully transferred conversation {conversation_id} to queue {target_queue_id}.")
return {}
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as http_err:
logger.error(f"HTTP error occurred: {http_err}")
logger.error(f"Response body: {response.text}")
# Specific error handling
if response.status_code == 404:
raise ValueError(f"Conversation {conversation_id} not found or does not exist.")
elif response.status_code == 403:
raise PermissionError("Insufficient permissions. Ensure the client has 'conversation:transfer:write'.")
elif response.status_code == 409:
raise RuntimeError("Conflict: The conversation is in a state that does not allow transfer (e.g., closed, ringing).")
else:
raise http_err
except requests.exceptions.RequestException as req_err:
logger.error(f"Request failed: {req_err}")
raise req_err
Complete Working Example
This script combines authentication, queue lookup, and the transfer action into a single runnable module. It assumes you have the conversation_id from an external source (e.g., a webhook or database).
#!/usr/bin/env python3
"""
Genesys Cloud Call Transfer Example
Transfers an active voice conversation to a specified queue.
"""
import sys
import requests
import time
import logging
from typing import Optional
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, environment: str = "my.genesys.cloud"):
self.client_id = client_id
self.client_secret = client_secret
self.environment = environment
self.token_url = f"https://{environment}/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0.0
def get_access_token(self) -> str:
if self.access_token and time.time() < self.token_expiry:
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"]
self.token_expiry = time.time() + token_data["expires_in"] - 60
return self.access_token
except requests.exceptions.RequestException as e:
logger.error(f"Failed to obtain access token: {e}")
raise
def find_queue_by_name(auth: GenesysAuth, queue_name: str) -> Optional[str]:
token = auth.get_access_token()
url = f"https://{auth.environment}/api/v2/search"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"query": {
"bool": {
"must": [
{"term": {"entityType": "queue"}},
{"match": {"name": queue_name}}
]
}
}
}
try:
response = requests.post(url, json=payload, headers=headers)
if response.status_code == 200:
results = response.json().get("results", [])
if results:
return results[0].get("id")
except Exception as e:
logger.error(f"Error finding queue: {e}")
return None
def transfer_call_to_queue(auth: GenesysAuth, conversation_id: str, target_queue_id: str) -> bool:
token = auth.get_access_token()
url = f"https://{auth.environment}/api/v2/conversations/voice/{conversation_id}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = {
"actions": [
{
"action": "transfer",
"toType": "queue",
"toId": target_queue_id
}
]
}
try:
response = requests.patch(url, json=payload, headers=headers)
if response.status_code == 204:
logger.info("Transfer successful (204 No Content).")
return True
elif response.status_code == 200:
logger.info(f"Transfer successful. Response: {response.json()}")
return True
response.raise_for_status()
return False
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP Error: {e}")
logger.error(f"Response: {response.text}")
return False
except Exception as e:
logger.error(f"Unexpected error: {e}")
return False
def main():
# Configuration
CLIENT_ID = "your_client_id_here"
CLIENT_SECRET = "your_client_secret_here"
ENVIRONMENT = "my.genesys.cloud" # e.g., "usw2.my.genesys.cloud"
CONVERSATION_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" # Replace with active conversation ID
TARGET_QUEUE_NAME = "Support Tier 2" # Replace with actual queue name
if CLIENT_ID == "your_client_id_here":
logger.error("Please update CLIENT_ID and CLIENT_SECRET in the script.")
sys.exit(1)
# Initialize Auth
auth = GenesysAuth(CLIENT_ID, CLIENT_SECRET, ENVIRONMENT)
# Step 1: Get Token
try:
auth.get_access_token()
except Exception as e:
logger.error("Authentication failed.")
sys.exit(1)
# Step 2: Find Queue ID
queue_id = find_queue_by_name(auth, TARGET_QUEUE_NAME)
if not queue_id:
logger.error(f"Queue '{TARGET_QUEUE_NAME}' not found.")
sys.exit(1)
logger.info(f"Found Queue ID: {queue_id}")
# Step 3: Transfer Call
success = transfer_call_to_queue(auth, CONVERSATION_ID, queue_id)
if success:
logger.info("Process completed successfully.")
else:
logger.error("Transfer failed.")
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 409 Conflict
- Cause: The conversation is in a state that does not permit transfer. Common invalid states include
closed,ended, orringing(if the call has not been answered yet). You can only transfer calls that arequeued,waiting, orconnected. - Fix: Verify the current state of the conversation using
GET /api/v2/conversations/voice/{conversationId}. If the state isconnected, ensure the agent is available to be dropped. If the state isqueued, the transfer will move the caller to the new queue immediately.
Error: 403 Forbidden
- Cause: The OAuth token used does not have the
conversation:transfer:writescope. Alternatively, the System User associated with the client credentials lacks the necessary permissions in the Genesys Cloud Admin console. - Fix:
- Check the client credentials in Genesys Cloud Admin > Security > Client Credentials. Ensure
conversation:transfer:writeis checked. - Verify the System User has the “Transfer conversations” permission.
- Check the client credentials in Genesys Cloud Admin > Security > Client Credentials. Ensure
Error: 404 Not Found
- Cause: The
conversationIdprovided does not exist, or it belongs to a different organization (unlikely if using correct credentials). It may also happen if the conversation has aged out of the active cache (conversations are typically retained for 24 hours in the API). - Fix: Use the Analytics API to look up historical conversations if the ID is older than 24 hours, or ensure you are capturing the ID during the active call lifecycle.
Error: 422 Unprocessable Entity
- Cause: The payload structure is incorrect. For example, using
toType: "user"when you intended a queue, or providing an invalid UUID format fortoId. - Fix: Ensure the
actionsarray contains exactly one object withaction: "transfer",toType: "queue", and a validtoId.