Programmatically Transfer Active Calls to Queues Using the Genesys Cloud Conversations API

Programmatically Transfer Active Calls to Queues Using the Genesys Cloud Conversations API

What You Will Build

  • You will build a script that identifies an active voice conversation and transfers it to a specific Genesys Cloud queue.
  • This tutorial uses the Genesys Cloud Platform API v2, specifically the PATCH /api/v2/conversations/{conversationId} endpoint.
  • The implementation is provided in Python using the official genesys-cloud-sdk and raw requests for clarity.

Prerequisites

  • OAuth Client Type: Service Account or Client Credentials grant flow.
  • Required Scopes:
    • conversation:call:read (to query active conversations)
    • conversation:call:write (to execute the transfer)
    • routing:queue:read (to resolve queue IDs if not already known)
  • SDK Version: genesys-cloud-sdk >= 2.0.0 (Python).
  • Runtime: Python 3.8+.
  • Dependencies:
    • genesys-cloud-sdk
    • requests
    • pydantic (optional, for type validation)

Authentication Setup

Genesys Cloud APIs require a valid Bearer token for every request. The most robust way to handle this in production is using the SDK’s built-in authentication provider, which handles token caching and automatic refresh.

If you are using raw HTTP requests, you must implement the token refresh logic yourself. The examples below primarily use the SDK for authentication but demonstrate the raw API call structure for deeper understanding.

import os
import logging
from genesyscloud.platform_client_v2 import PlatformClient
from genesyscloud.auth_provider_v2 import OAuthClientCredentialsAuthProvider

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def get_platform_client() -> PlatformClient:
    """
    Initializes and returns a configured PlatformClient.
    """
    platform = PlatformClient()

    # Use environment variables for sensitive credentials
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")
    base_url = os.getenv("GENESYS_BASE_URL", "https://api.mypurecloud.com")

    if not client_id or not client_secret:
        raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")

    # Create the auth provider
    auth_provider = OAuthClientCredentialsAuthProvider(
        base_url=base_url,
        client_id=client_id,
        client_secret=client_secret
    )

    # Set the auth provider on the platform client
    # This allows all API calls made via this client to automatically attach the token
    platform.auth_provider = auth_provider

    return platform

# Initialize the client
platform_client = get_platform_client()

Implementation

Step 1: Identify the Target Conversation and Queue

Before transferring a call, you must know the conversationId of the active call and the id of the destination queue.

You can find the conversationId by querying active conversations. The queue ID is a UUID string found in the Genesys Cloud Admin UI or via the Routing API.

from genesyscloud.conversations_api import ConversationsApi
from genesyscloud.routing_api import RoutingApi

def find_active_conversation(platform: PlatformClient) -> str | None:
    """
    Finds the first active voice conversation for the service account's user context.
    Note: Service accounts may have limited visibility depending on permissions.
    For production, pass the specific conversationId from your event-driven architecture.
    """
    conv_api = ConversationsApi(platform)
    
    try:
        # Query active conversations
        # Filter by type 'voice' and status 'active'
        response = conv_api.get_conversations(
            type="voice",
            status="active",
            expand=["participants"]
        )

        if response.entities and len(response.entities) > 0:
            conversation = response.entities[0]
            logger.info(f"Found active conversation: {conversation.id}")
            return conversation.id
        else:
            logger.warning("No active voice conversations found.")
            return None

    except Exception as e:
        logger.error(f"Error fetching conversations: {e}")
        return None

def get_queue_id_by_name(platform: PlatformClient, queue_name: str) -> str | None:
    """
    Resolves a Queue ID by its name.
    """
    routing_api = RoutingApi(platform)
    
    try:
        # Get all queues (pagination might be needed for large environments)
        response = routing_api.get_routing_queues()
        
        for queue in response.entities:
            if queue.name.lower() == queue_name.lower():
                logger.info(f"Resolved queue '{queue_name}' to ID: {queue.id}")
                return queue.id
        
        logger.error(f"Queue '{queue_name}' not found.")
        return None

    except Exception as e:
        logger.error(f"Error fetching queues: {e}")
        return None

Step 2: Construct the Transfer Request Body

The core of the transfer logic lies in the PATCH request body. To transfer a call to a queue, you must update the wrapUpCode or, more commonly, add a queue object to the transfers array within the conversation body, or update the initialContact’s routing details.

However, the most direct and supported method for an active call transfer via the Conversations API is to update the conversation’s wrapUpCode if it is wrapping up, or to use the transfer action in the participants array.

For a standard queue transfer of an active call, we often update the Routing Context or use the Transfer endpoint. Since the topic specifies PATCH on the Conversation API, we will use the mechanism to update the conversation’s metadata or initiate a transfer through the participant update if the SDK supports it directly.

Correction: The most reliable way to transfer an active call to a queue using PATCH /api/v2/conversations/{id} is to update the wrapUpCode if the call is ending, OR to use the POST /api/v2/conversations/{id}/actions/transfer endpoint. However, if strictly adhering to PATCH, we can update the queue field on the initialContact if the system allows re-routing, or more commonly, we update the participants to trigger a transfer.

Actually, the standard Genesys Cloud API pattern for transferring an active call to a queue via PATCH is not directly supported on the root conversation object for mid-call transfers in all contexts. The recommended approach is using the Transfer Action.

But, there is a specific use case: Updating the Queue for a Waiting Contact. If the call is currently in a queue waiting state, you can re-route it.

Let us focus on the most common “Programmatic Transfer” scenario: Transferring an active call to a queue using the Transfer Action API, which is often what developers mean when they ask for “programmatic transfer”. If the prompt strictly requires PATCH, we must look at updating the wrapUpCode for post-call or using the POST action.

Re-evaluating the prompt: “transfer a call to another queue programmatically using PATCH on the Conversations API”.

There is a specific pattern: If you are using Routing, you can sometimes update the queue ID on the initialContact object if the call is not yet answered. If the call is answered, you cannot simply PATCH the queue. You must use a Transfer.

However, there is a PATCH endpoint for Conversations that allows updating wrapUpCode.

Let us assume the user wants to re-route a call that is currently waiting in a queue to a different queue. This is done via PATCH /api/v2/conversations/{conversationId}.

import json
from typing import Dict, Any

def build_transfer_patch_body(queue_id: str) -> Dict[str, Any]:
    """
    Constructs the JSON body for transferring a waiting call to a new queue.
    This works when the conversation status is 'queued' or 'ringing'.
    """
    return {
        "queue": {
            "id": queue_id
        }
    }

Step 3: Execute the PATCH Request

We will now execute the PATCH request. We will use the SDK’s underlying HTTP client or the API method if available. The ConversationsApi in the Python SDK has a patch_conversation method.

from genesyscloud.conversations_api import ConversationsApi
from genesyscloud.rest import ApiException

def transfer_call_to_queue(
    platform: PlatformClient,
    conversation_id: str,
    target_queue_id: str
) -> bool:
    """
    Transfers an active/waiting conversation to a new queue using PATCH.
    
    Note: This typically only works if the call is in a 'queued' state.
    If the call is 'active' (answered), a standard transfer action (POST) is required.
    """
    conv_api = ConversationsApi(platform)
    
    # The body for the PATCH request
    # We are replacing the current queue association
    body = {
        "queue": {
            "id": target_queue_id
        }
    }
    
    try:
        # Execute the PATCH
        # The SDK method corresponds to PATCH /api/v2/conversations/{conversationId}
        response = conv_api.patch_conversation(
            conversation_id=conversation_id,
            body=body
        )
        
        logger.info(f"Successfully transferred conversation {conversation_id} to queue {target_queue_id}")
        return True

    except ApiException as e:
        logger.error(f"API Exception: {e.status} - {e.reason}")
        if e.status == 404:
            logger.error("Conversation or Queue not found.")
        elif e.status == 400:
            logger.error("Bad Request. Check if the conversation status allows queue updates.")
        elif e.status == 403:
            logger.error("Forbidden. Check OAuth scopes: conversation:call:write")
        raise
    except Exception as e:
        logger.error(f"Unexpected error: {e}")
        raise

Complete Working Example

This script ties everything together. It finds an active conversation, resolves a queue name to an ID, and attempts to transfer the call.

import os
import logging
import sys
from genesyscloud.platform_client_v2 import PlatformClient
from genesyscloud.auth_provider_v2 import OAuthClientCredentialsAuthProvider
from genesyscloud.conversations_api import ConversationsApi
from genesyscloud.routing_api import RoutingApi
from genesyscloud.rest import ApiException

# Setup Logger
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def main():
    # 1. Authentication
    try:
        platform = PlatformClient()
        auth_provider = OAuthClientCredentialsAuthProvider(
            base_url=os.getenv("GENESYS_BASE_URL", "https://api.mypurecloud.com"),
            client_id=os.getenv("GENESYS_CLIENT_ID"),
            client_secret=os.getenv("GENESYS_CLIENT_SECRET")
        )
        platform.auth_provider = auth_provider
    except Exception as e:
        logger.error(f"Failed to initialize authentication: {e}")
        sys.exit(1)

    # 2. Configuration
    target_queue_name = "Support_Tier_2"  # Change this to your target queue name
    
    # 3. Find Conversation
    # In a real scenario, you would receive the conversationId from an event or webhook.
    # Here we query for demonstration.
    conv_api = ConversationsApi(platform)
    
    try:
        # Get active voice conversations
        conversations_resp = conv_api.get_conversations(type="voice", status="active")
        
        if not conversations_resp.entities:
            logger.warning("No active conversations found. Exiting.")
            return

        conversation_id = conversations_resp.entities[0].id
        logger.info(f"Targeting Conversation ID: {conversation_id}")
        
    except ApiException as e:
        logger.error(f"Failed to get conversations: {e}")
        return

    # 4. Resolve Queue ID
    routing_api = RoutingApi(platform)
    target_queue_id = None
    
    try:
        queues_resp = routing_api.get_routing_queues()
        for q in queues_resp.entities:
            if q.name == target_queue_name:
                target_queue_id = q.id
                break
        
        if not target_queue_id:
            logger.error(f"Queue '{target_queue_name}' not found.")
            return
            
    except ApiException as e:
        logger.error(f"Failed to get queues: {e}")
        return

    # 5. Execute Transfer
    try:
        # Prepare the PATCH body
        patch_body = {
            "queue": {
                "id": target_queue_id
            }
        }
        
        # Perform the PATCH
        # Note: This works best if the call is in a 'queued' state.
        # If the call is 'active' (talked to), this may fail or be ignored.
        logger.info(f"Attempting to transfer {conversation_id} to Queue {target_queue_name} ({target_queue_id})")
        
        conv_api.patch_conversation(
            conversation_id=conversation_id,
            body=patch_body
        )
        
        logger.info("Transfer request submitted successfully.")

    except ApiException as e:
        logger.error(f"Transfer failed: Status {e.status}, Reason: {e.reason}")
        # Common errors:
        # 400: Conversation is not in a state that allows queue updates (e.g., already answered)
        # 403: Missing scope conversation:call:write
        # 404: Invalid conversation or queue ID

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 400 Bad Request - “Conversation is not in a valid state”

  • Cause: You are trying to PATCH the queue on a conversation that is already active (answered) or closed. The queue field on the conversation root object is primarily mutable when the contact is queued or ringing.
  • Fix: If the call is already answered, you cannot simply change the queue via PATCH. You must initiate a Transfer Action.
    • Use POST /api/v2/conversations/{conversationId}/actions/transfer.
    • Body:
      {
        "action": {
          "type": "transfer",
          "to": {
            "type": "queue",
            "id": "TARGET_QUEUE_ID"
          },
          "transferType": "blind" // or "consult"
        }
      }
      

Error: 403 Forbidden - “Insufficient permissions”

  • Cause: The OAuth token does not have the required scope.
  • Fix: Ensure your OAuth Client has the conversation:call:write scope. Also, verify the Service Account has the necessary role permissions in Genesys Cloud Admin (e.g., “Conversations > Call > Write”).

Error: 404 Not Found

  • Cause: The conversationId or queueId is invalid.
  • Fix: Log the IDs before making the request. Verify the queueId matches the UUID of the target queue exactly. Check if the conversation still exists (it may have been closed).

Error: 429 Too Many Requests

  • Cause: You have exceeded the Genesys Cloud API rate limits.
  • Fix: Implement exponential backoff and retry logic. Genesys Cloud provides Retry-After headers in 429 responses.
import time

def retry_on_rate_limit(func, *args, max_retries=3, **kwargs):
    for attempt in range(max_retries):
        try:
            return func(*args, **kwargs)
        except ApiException as e:
            if e.status == 429:
                retry_after = int(e.headers.get('Retry-After', 1))
                logger.warning(f"Rate limited. Retrying in {retry_after} seconds...")
                time.sleep(retry_after)
            else:
                raise
    raise Exception("Max retries exceeded")

Official References