Transfer a Call to Another Queue Programmatically via Genesys Cloud Conversations API
What You Will Build
- A script that identifies an active voice conversation and transfers it from its current queue to a target queue.
- The solution uses the Genesys Cloud Platform API v2 Conversations endpoints and the Python SDK.
- The implementation covers Python, with logic applicable to JavaScript and Java SDKs.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth client with the
client_credentialsgrant type. - Required Scopes:
conversation:read,conversation:write,routing:queue:read. - SDK Version:
genesyscloud-pythonversion 140.0.0 or higher. - Runtime: Python 3.8+.
- Dependencies:
genesyscloud,pydantic(bundled with SDK).
Authentication Setup
The Genesys Cloud Python SDK handles token acquisition and caching internally. You must initialize the PlatformClient with your client credentials. The SDK caches the access token in memory. If the token expires, the SDK automatically attempts to refresh it if the grant type supports it, or throws an error requiring re-initialization for client_credentials.
from purecloudplatformclientv2 import (
ApiClient,
Configuration,
ConversationApi,
RoutingApi
)
# Configuration
GENESYS_CLOUD_DOMAIN = "https://api.mypurecloud.com"
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
def get_platform_client() -> Configuration:
"""
Initializes and returns the Genesys Cloud Configuration object.
"""
config = Configuration(
host=GENESYS_CLOUD_DOMAIN,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET
)
return config
def get_api_client(config: Configuration) -> ApiClient:
"""
Creates an ApiClient instance. This handles HTTP requests and token management.
"""
return ApiClient(config)
Implementation
Step 1: Identify the Active Conversation
Before transferring a call, you must locate the specific conversation ID. The Conversations API allows you to query active conversations. For this tutorial, we assume you already have the conversationId. If you do not, you can fetch active voice conversations using the GET /api/v2/conversations/voice/active endpoint.
Endpoint: GET /api/v2/conversations/voice/active
Scope: conversation:read
def find_active_conversation(api_client: ApiClient, participant_email: str) -> str | None:
"""
Finds an active voice conversation associated with a specific participant email.
In a production environment, you might use a correlation ID or external contact ID.
"""
conv_api = ConversationApi(api_client)
# Fetch active voice conversations
# Limit to 100 for this example. In production, handle pagination if >100 active calls.
response = conv_api.get_voice_active(limit=100)
for conv in response.entities:
for participant in conv.participants:
# Check if the participant is the customer (external) and matches the email/identifier
if participant.address_type == "email" and participant.address == participant_email:
return conv.id
return None
Step 2: Locate the Target Queue ID
The PATCH request requires the routing.queueId of the destination queue. You must resolve the queue name to its unique ID.
Endpoint: GET /api/v2/routing/queues
Scope: routing:queue:read
def get_queue_id(api_client: ApiClient, queue_name: str) -> str | None:
"""
Resolves a queue name to its Genesys Cloud Queue ID.
"""
routing_api = RoutingApi(api_client)
# Search for queues matching the name
# The API supports filtering by name, but exact match logic is safer in code
response = routing_api.get_routing_queues(name=queue_name)
for queue in response.entities:
if queue.name == queue_name:
return queue.id
return None
Step 3: Execute the Transfer via PATCH
This is the core operation. To transfer a call to another queue, you must use the PATCH /api/v2/conversations/voice/{conversationId} endpoint.
Critical Logic:
- You cannot simply change the
routing.queueIdon the conversation object directly in all contexts. The standard pattern for a “blind transfer” or “queue transfer” via API is to update the routing context of the conversation. - However, for a true programmatic transfer that mimics an agent transferring a call, you often need to ensure the conversation is still active and eligible.
- The most reliable method for a system-initiated queue transfer is to update the
routingproperty of the conversation entity.
Endpoint: PATCH /api/v2/conversations/voice/{conversationId}
Scope: conversation:write
Request Body Structure:
The body must contain the routing object with the new queueId.
{
"routing": {
"queueId": "new-queue-id-here"
}
}
Code Implementation:
def transfer_call_to_queue(api_client: ApiClient, conversation_id: str, target_queue_id: str) -> bool:
"""
Transfers an active voice conversation to a new queue.
Args:
api_client: The initialized ApiClient.
conversation_id: The UUID of the conversation to transfer.
target_queue_id: The UUID of the destination queue.
Returns:
True if successful, False otherwise.
"""
conv_api = ConversationApi(api_client)
# Construct the routing update payload
# Note: The Python SDK uses specific model classes.
# We use the ConversationRouting class for the payload.
from purecloudplatformclientv2 import ConversationRouting
routing_update = ConversationRouting(
queue_id=target_queue_id
)
# The Conversation object for PATCH requires the routing property to be set
# We do not need to send the entire conversation object, only the changed fields.
from purecloudplatformclientv2 import Conversation
# Create a partial Conversation object with only the routing field
# This ensures we do not accidentally overwrite other properties like 'wrapup_code'
conv_patch_body = Conversation(
routing=routing_update
)
try:
# Execute the PATCH request
# The SDK method is patch_conversations_voice_conversation
response = conv_api.patch_conversations_voice_conversation(
conversation_id=conversation_id,
body=conv_patch_body
)
print(f"Call transferred successfully. New Queue ID: {response.routing.queue_id}")
return True
except Exception as e:
# Handle API errors
status_code = e.status if hasattr(e, 'status') else None
reason = e.reason if hasattr(e, 'reason') else str(e)
if status_code == 429:
print(f"Rate limit exceeded. Reason: {reason}. Implement retry logic.")
elif status_code == 404:
print(f"Conversation {conversation_id} not found or ended.")
elif status_code == 400:
print(f"Bad Request: {reason}. Check if the queue ID is valid and the conversation is active.")
else:
print(f"Unexpected error: {reason}")
return False
Step 4: Verify the Transfer
After the PATCH request returns a 200 OK, you should verify that the conversation has indeed moved to the new queue. This is crucial for idempotency and debugging.
def verify_transfer(api_client: ApiClient, conversation_id: str) -> dict:
"""
Fetches the conversation details to verify the current queue ID.
"""
conv_api = ConversationApi(api_client)
try:
response = conv_api.get_conversations_voice_conversation(conversation_id=conversation_id)
return {
"conversation_id": response.id,
"current_queue_id": response.routing.queue_id if response.routing else None,
"state": response.state
}
except Exception as e:
print(f"Failed to verify transfer: {e}")
return {}
Complete Working Example
This script combines all steps into a single executable module. It assumes you have an active conversation ID and a target queue name.
#!/usr/bin/env python3
"""
Genesys Cloud Call Transfer Script
Transfers an active voice conversation to a specified queue.
"""
import os
from purecloudplatformclientv2 import (
ApiClient,
Configuration,
ConversationApi,
RoutingApi,
ConversationRouting,
Conversation
)
# Configuration Constants
GENESYS_CLOUD_DOMAIN = os.getenv("GENESYS_CLOUD_DOMAIN", "https://api.mypurecloud.com")
CLIENT_ID = os.getenv("GENESYS_CLOUD_CLIENT_ID")
CLIENT_SECRET = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
def initialize_api_client() -> ApiClient:
"""Initializes the Genesys Cloud API client."""
if not CLIENT_ID or not CLIENT_SECRET:
raise ValueError("GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET must be set.")
config = Configuration(
host=GENESYS_CLOUD_DOMAIN,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET
)
return ApiClient(config)
def get_queue_id_by_name(api_client: ApiClient, queue_name: str) -> str | None:
"""Resolves queue name to ID."""
routing_api = RoutingApi(api_client)
try:
# Use the name filter parameter
response = routing_api.get_routing_queues(name=queue_name)
for queue in response.entities:
if queue.name == queue_name:
return queue.id
except Exception as e:
print(f"Error fetching queue '{queue_name}': {e}")
return None
def transfer_call(api_client: ApiClient, conversation_id: str, target_queue_id: str) -> bool:
"""Performs the PATCH operation to transfer the call."""
conv_api = ConversationApi(api_client)
# Prepare the payload
routing_update = ConversationRouting(queue_id=target_queue_id)
conv_patch_body = Conversation(routing=routing_update)
try:
conv_api.patch_conversations_voice_conversation(
conversation_id=conversation_id,
body=conv_patch_body
)
print(f"Success: Conversation {conversation_id} transferred to Queue {target_queue_id}")
return True
except Exception as e:
status = e.status if hasattr(e, 'status') else "Unknown"
print(f"Error transferring call (Status {status}): {e.reason if hasattr(e, 'reason') else e}")
return False
def main():
# Input Parameters
TARGET_QUEUE_NAME = "Technical Support"
CONVERSATION_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" # Replace with real ID
print(f"Starting transfer process for Conversation: {CONVERSATION_ID}")
print(f"Target Queue: {TARGET_QUEUE_NAME}")
# 1. Initialize Client
api_client = initialize_api_client()
# 2. Get Target Queue ID
target_queue_id = get_queue_id_by_name(api_client, TARGET_QUEUE_NAME)
if not target_queue_id:
print(f"Error: Could not find queue named '{TARGET_QUEUE_NAME}'. Aborting.")
return
print(f"Found Target Queue ID: {target_queue_id}")
# 3. Execute Transfer
success = transfer_call(api_client, CONVERSATION_ID, target_queue_id)
if success:
print("Transfer completed successfully.")
else:
print("Transfer failed.")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 400 Bad Request - “Queue ID is invalid” or “Conversation is not active”
Cause: The conversation has ended, or the queueId provided does not exist or is not accessible to the OAuth client.
Fix:
- Verify the conversation is in
activeorqueuedstate. Transferring anendedconversation is invalid. - Ensure the OAuth client has
routing:queue:readandrouting:queue:writescopes if the queue has specific permissions. - Check that the
queueIdis a valid UUID.
# Debugging Code: Check Conversation State
def check_conversation_state(api_client: ApiClient, conversation_id: str):
conv_api = ConversationApi(api_client)
try:
conv = conv_api.get_conversations_voice_conversation(conversation_id)
print(f"State: {conv.state}")
print(f"Current Queue: {conv.routing.queue_id}")
except Exception as e:
print(f"Error: {e}")
Error: 403 Forbidden - “Insufficient permissions”
Cause: The OAuth client lacks the conversation:write scope.
Fix: Add conversation:write to the OAuth client’s scopes in the Genesys Cloud Admin console.
Error: 429 Too Many Requests
Cause: You are hitting the API rate limit. The Conversations API has a relatively high limit, but rapid polling or bulk transfers can trigger it.
Fix: Implement exponential backoff.
import time
def transfer_with_retry(api_client: ApiClient, conversation_id: str, target_queue_id: str, max_retries=3):
for attempt in range(max_retries):
try:
return transfer_call(api_client, conversation_id, target_queue_id)
except Exception as e:
if hasattr(e, 'status') and e.status == 429:
wait_time = 2 ** attempt
print(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time)
else:
raise
return False
Error: Conversation Does Not Move
Cause: The conversation might be in a state where it cannot be transferred, such as held by an agent or already transferred.
Fix: Ensure the conversation is in a queued or active state and not already associated with a different routing context that locks it. If the call is already with an agent, you cannot simply PATCH the queue; you must use the Transfer API (POST /api/v2/conversations/voice/{conversationId}/transfers) to initiate a blind or consult transfer. The PATCH method is primarily for system-level queue reassignment for calls in queue or system-managed states.