How to programmatically close a Web Messaging session from the backend

How to programmatically close a Web Messaging session from the backend

What You Will Build

  • You will build a backend service that programmatically terminates an active Genesys Cloud Web Messaging conversation.
  • This tutorial uses the Genesys Cloud Platform API v2 (/api/v2/conversations/messaging/) and the Python genesyscloud SDK.
  • The implementation is written in Python 3.9+ using the asyncio framework for high-concurrency handling.

Prerequisites

  • OAuth Client Type: Machine-to-Machine (M2M) or Public Client with appropriate scopes.
  • Required Scopes: conversation:message:write is mandatory to update conversation status. conversation:read is required if you need to fetch conversation details before closing.
  • SDK Version: genesyscloud Python SDK version 140.0.0 or higher.
  • Runtime: Python 3.9 or higher.
  • Dependencies:
    • genesyscloud
    • pydantic (for data validation, optional but recommended)

Install the SDK via pip:

pip install genesyscloud

Authentication Setup

Genesys Cloud uses OAuth 2.0 for authentication. For backend services, the Machine-to-Machine (M2M) flow is the standard. This flow exchanges a client ID and client secret for an access token.

The genesyscloud SDK handles token caching and automatic refresh if you configure the PlatformClient correctly. You must provide your client_id, client_secret, and environment (e.g., mypurecloud.com or usw2.pure.cloud).

import os
from purecloudplatformclientv2 import PlatformClient

def get_platform_client() -> PlatformClient:
    """
    Initialize and return the Genesys Cloud Platform Client.
    """
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")
    environment = os.getenv("GENESYS_ENVIRONMENT", "mypurecloud.com")

    if not client_id or not client_secret:
        raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set.")

    pc = PlatformClient()
    pc.set_environment(environment)
    pc.set_client_credentials(client_id, client_secret)
    
    # Enable automatic token refresh
    pc.enable_auto_refresh()
    
    return pc

Implementation

Step 1: Retrieve the Conversation ID

To close a session, you need the unique conversationId. In a typical Web Messaging integration, the frontend generates a temporary session ID, but the backend must map this to the Genesys Cloud conversationId if it is not passed directly.

If your frontend sends the conversationId directly (recommended for backend-initiated actions), you can skip retrieval. If you only have the sessionKey (the temporary ID used by the widget), you must query the conversation API.

However, the most robust backend pattern is to store the mapping between your internal user/session ID and the Genesys conversationId when the conversation starts. This tutorial assumes you have the conversationId.

If you do not have it, you can list recent conversations for a specific user or queue. This is expensive and should be avoided in production. Instead, ensure your frontend passes the conversationId to your backend via a secure HTTP header or body payload upon session start.

Step 2: Construct the Close Payload

To close a Web Messaging conversation, you must perform a PATCH request to the conversation endpoint. The genesyscloud SDK provides the ConversationMessagingApi class.

The key parameter is status. For Web Messaging, the valid statuses are active, closed, and abandoned. To close the session cleanly, set status to closed.

You must also provide the closeReason if your Genesys Cloud organization requires it. This is configured in the Messaging settings. If not configured, you can omit it, but it is best practice to include a reason for analytics.

The request body requires a ConversationUpdate object.

from purecloudplatformclientv2 import (
    ConversationMessagingApi,
    ConversationUpdate,
    ConversationStatus
)

def prepare_close_payload(conversation_id: str, close_reason: str = "Agent initiated close") -> dict:
    """
    Prepare the payload for closing a conversation.
    """
    # Create the ConversationUpdate object
    update_body = ConversationUpdate(
        status="closed",  # Set status to closed
        close_reason=close_reason  # Optional but recommended
    )
    
    return {
        "conversation_id": conversation_id,
        "body": update_body
    }

Step 3: Execute the Close Request

Use the patch_conversations_messaging_conversation method. This method sends the PATCH request to /api/v2/conversations/messaging/{conversationId}.

You must handle potential errors:

  • 404 Not Found: The conversation ID is invalid or does not exist.
  • 409 Conflict: The conversation is already closed or in a state that prevents closing.
  • 403 Forbidden: The OAuth token lacks the conversation:message:write scope.
  • 429 Too Many Requests: Rate limiting has been triggered.
import logging
from purecloudplatformclientv2.rest import ApiException

logger = logging.getLogger(__name__)

async def close_web_messaging_session(pc: PlatformClient, conversation_id: str) -> bool:
    """
    Programmatically close a Web Messaging conversation.
    
    Args:
        pc: The configured PlatformClient instance.
        conversation_id: The unique ID of the conversation to close.
        
    Returns:
        True if the conversation was successfully closed, False otherwise.
    """
    api_instance = ConversationMessagingApi(pc)
    
    try:
        # Prepare the update body
        update_body = ConversationUpdate(
            status="closed",
            close_reason="Backend system close"
        )
        
        # Execute the patch request
        # The SDK method name is patch_conversations_messaging_conversation
        api_instance.patch_conversations_messaging_conversation(
            conversation_id=conversation_id,
            body=update_body
        )
        
        logger.info(f"Successfully closed conversation {conversation_id}")
        return True
        
    except ApiException as e:
        logger.error(f"API Exception when closing conversation {conversation_id}: {e.status} - {e.reason}")
        
        if e.status == 404:
            logger.warning(f"Conversation {conversation_id} not found.")
        elif e.status == 409:
            logger.warning(f"Conversation {conversation_id} is already closed or in an invalid state.")
        elif e.status == 403:
            logger.error(f"Permission denied. Check OAuth scopes for conversation:message:write.")
        elif e.status == 429:
            logger.warning("Rate limit exceeded. Implement exponential backoff.")
            
        return False
    except Exception as e:
        logger.error(f"Unexpected error closing conversation {conversation_id}: {str(e)}")
        return False

Step 4: Handling Webhook Triggers (Optional but Recommended)

Often, you want to close a conversation automatically based on an event, such as an agent wrapping up. You can achieve this by listening to the conversation:statusChanged webhook.

When the webhook fires, check if the status is wrapup or closed. If it is wrapup and you want to force-close the messaging session immediately, invoke the close_web_messaging_session function.

This ensures that the frontend widget updates immediately to reflect the closed state, rather than waiting for the agent to manually click “Close”.

Complete Working Example

This is a complete, runnable Python script. It initializes the client, accepts a conversation ID from the command line, and closes the session.

import os
import sys
import asyncio
import logging
from purecloudplatformclientv2 import PlatformClient, ConversationMessagingApi, ConversationUpdate
from purecloudplatformclientv2.rest import ApiException

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

def get_platform_client() -> PlatformClient:
    """
    Initialize the Genesys Cloud Platform Client with M2M OAuth.
    """
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")
    environment = os.getenv("GENESYS_ENVIRONMENT", "mypurecloud.com")

    if not client_id or not client_secret:
        raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables must be set.")

    pc = PlatformClient()
    pc.set_environment(environment)
    pc.set_client_credentials(client_id, client_secret)
    pc.enable_auto_refresh()
    
    return pc

async def close_conversation(conversation_id: str) -> None:
    """
    Main function to close a Web Messaging conversation.
    """
    pc = get_platform_client()
    api_instance = ConversationMessagingApi(pc)
    
    # Define the update payload
    # Status must be 'closed' for Web Messaging
    update_body = ConversationUpdate(
        status="closed",
        close_reason="Programmatic close via backend"
    )
    
    try:
        logger.info(f"Attempting to close conversation: {conversation_id}")
        
        # Perform the PATCH request
        # Note: The SDK method returns the updated conversation object
        updated_conversation = api_instance.patch_conversations_messaging_conversation(
            conversation_id=conversation_id,
            body=update_body
        )
        
        logger.info(f"Conversation {conversation_id} closed successfully. New status: {updated_conversation.status}")
        
    except ApiException as e:
        logger.error(f"Failed to close conversation {conversation_id}. Status: {e.status}, Reason: {e.reason}")
        logger.error(f"Response body: {e.body}")
        sys.exit(1)
    except Exception as e:
        logger.error(f"Unexpected error: {str(e)}")
        sys.exit(1)

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python close_session.py <conversation_id>")
        sys.exit(1)
    
    conversation_id = sys.argv[1]
    asyncio.run(close_conversation(conversation_id))

Common Errors & Debugging

Error: 403 Forbidden - Insufficient Scope

What causes it: The OAuth token used by the PlatformClient does not have the conversation:message:write scope.

How to fix it:

  1. Go to the Genesys Cloud Admin Portal.
  2. Navigate to Applications and Integrations > API Applications.
  3. Select your client ID.
  4. Edit the Scopes.
  5. Add conversation:message:write to the list.
  6. Save the changes.
  7. Generate a new token (the SDK will do this automatically on next call if enable_auto_refresh() is true, but ensure the client credentials are correct).

Code Fix: Verify the scope in your OAuth token payload (decode the JWT) or check the Admin Console.

Error: 409 Conflict - Conversation Already Closed

What causes it: The conversation is already in the closed status, or it is in a state that cannot be transitioned to closed (e.g., already abandoned).

How to fix it:
Check the current status before attempting to close. If the status is already closed, skip the API call.

# Check status before closing
def is_conversation_closed(conversation_id: str, pc: PlatformClient) -> bool:
    api_instance = ConversationMessagingApi(pc)
    try:
        conv = api_instance.get_conversations_messaging_conversation(conversation_id)
        return conv.status == "closed"
    except ApiException as e:
        if e.status == 404:
            return False
        raise

Error: 429 Too Many Requests

What causes it: You are sending close requests faster than the API rate limit allows. Genesys Cloud enforces rate limits per client ID and per endpoint.

How to fix it:
Implement exponential backoff. The genesyscloud SDK does not automatically retry all requests, so you must handle this in your code.

import time

async def close_with_retry(pc: PlatformClient, conversation_id: str, max_retries: int = 3) -> bool:
    for attempt in range(max_retries):
        try:
            return await close_web_messaging_session(pc, conversation_id)
        except ApiException as e:
            if e.status == 429:
                wait_time = 2 ** attempt  # Exponential backoff: 1s, 2s, 4s
                logger.warning(f"Rate limited. Retrying in {wait_time} seconds...")
                await asyncio.sleep(wait_time)
            else:
                raise
    return False

Error: 400 Bad Request - Invalid Status

What causes it: You passed an invalid status value. For Web Messaging, the only valid statuses are active, closed, and abandoned. You cannot set it to wrapup or queued via this API.

How to fix it:
Ensure the status field in ConversationUpdate is exactly "closed".

Official References