How to set wrap-up codes programmatically after an interaction ends

How to set wrap-up codes programmatically after an interaction ends

What You Will Build

  • You will build a service that detects when a conversation (voice, chat, or email) transitions to the WRAPPED state and applies a specific wrap-up code via the API.
  • This tutorial uses the Genesys Cloud CX REST API and the Python SDK (genesys-cloud-purecloud-platform-client).
  • The implementation covers Python, demonstrating the use of Webhooks for event detection and the Analytics API for data retrieval.

Prerequisites

  • OAuth Client Type: A Genesys Cloud OAuth Client configured with Client Credentials flow.
  • Required Scopes:
    • conversation:read (to retrieve conversation details)
    • routing:wrapup:write (to set the wrap-up code)
    • analytics:conversations:read (optional, for advanced querying if not using webhooks directly)
  • SDK Version: genesys-cloud-purecloud-platform-client >= 158.0.0.
  • Language/Runtime: Python 3.9+.
  • External Dependencies:
    • genesys-cloud-purecloud-platform-client
    • requests (for webhook verification if needed, though SDK handles most)

Authentication Setup

Genesys Cloud APIs require a valid OAuth 2.0 Bearer token. For server-to-server interactions, such as setting wrap-up codes programmatically, the Client Credentials flow is the standard approach. This flow does not require user interaction and relies on the client ID and secret associated with your OAuth application.

The following code initializes the Genesys Cloud Python SDK with the necessary authentication configuration. We use a Configuration object to store the base URL and the token retrieval logic.

import os
from genesyscloud import Configuration, AuthClient

def get_genesys_config() -> Configuration:
    """
    Initializes the Genesys Cloud configuration with OAuth client credentials.
    """
    # Retrieve credentials from environment variables
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")
    
    if not client_id or not client_secret:
        raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in environment.")

    # Create configuration object
    config = Configuration()
    
    # Set the OAuth client credentials
    config.oauth_client_id = client_id
    config.oauth_client_secret = client_secret
    
    # Optional: Set the region if not using the default (mypurecloud.com)
    # config.host = "https://api.euw1.pure.cloud"
    
    return config

# Initialize the configuration
genesys_config = get_genesys_config()

This configuration object will be used to instantiate API clients throughout the tutorial. The SDK handles token caching and automatic refresh, so you do not need to manually manage token expiration in most cases.

Implementation

Step 1: Define the Wrap-Up Code Logic

Before writing the trigger, you must identify the ID of the wrap-up code you wish to apply. Wrap-up codes are defined within Routing Queues or Skills. You cannot apply a wrap-up code that does not exist in the system.

First, we retrieve the available wrap-up codes for a specific queue. This is necessary because the API requires the wrapupCodeId and, in some contexts, the wrapupCodeName.

from genesyscloud import RoutingApi
from genesyscloud.rest import ApiException

def get_wrapup_code_id(queue_id: str, wrapup_code_name: str) -> str:
    """
    Retrieves the ID of a wrap-up code by name within a specific queue.
    
    Args:
        queue_id: The ID of the routing queue.
        wrapup_code_name: The name of the wrap-up code.
        
    Returns:
        The wrap-up code ID string.
        
    Raises:
        ApiException: If the API call fails.
    """
    # Initialize the Routing API client
    routing_api = RoutingApi(genesys_config)
    
    try:
        # Retrieve queue details
        queue = routing_api.get_routing_queue(
            queue_id=queue_id,
            expand=["wrapupcodes"]  # Crucial: must expand wrapupcodes to see them
        )
        
        if not queue.wrapup_codes:
            raise ValueError("No wrap-up codes found in the specified queue.")
            
        for code in queue.wrapup_codes:
            if code.name == wrapup_code_name:
                return code.id
                
        raise ValueError(f"Wrap-up code '{wrapup_code_name}' not found in queue {queue_id}.")
        
    except ApiException as e:
        print(f"Error retrieving queue: {e}")
        raise

OAuth Scope Required: routing:queue:read

Step 2: Handle the Webhook Trigger

The most efficient way to set a wrap-up code programmatically is to listen for the conversation:state:changed webhook. When a conversation moves to the WRAPPED state, the system allows you to modify the wrap-up code. However, there is a race condition: if you attempt to set a wrap-up code after the agent has already manually set one, the API will return a 409 Conflict.

We will create a handler function that processes the webhook payload. The payload contains the conversationId and the state.

from genesyscloud import ConversationApi
from genesyscloud.models import ConversationWrapup

def handle_conversation_state_change(payload: dict) -> None:
    """
    Processes a conversation state change webhook.
    If the state is 'WRAPPED' and no wrap-up code is set, it applies the default code.
    """
    conversation_id = payload.get("conversationId")
    state = payload.get("state")
    
    # Only process if the conversation has entered the WRAPPED state
    if state != "WRAPPED":
        return
        
    # Initialize Conversation API
    conversation_api = ConversationApi(genesys_config)
    
    try:
        # 1. Retrieve the current conversation details to check existing wrap-up
        conversation = conversation_api.get_conversations_conversation(
            conversation_id=conversation_id
        )
        
        # 2. Check if a wrap-up code is already set
        # Note: conversation.wrapup_code might be None if not set by agent
        if conversation.wrapup_code and conversation.wrapup_code.id:
            print(f"Wrap-up code already set for conversation {conversation_id}. Skipping.")
            return
            
        # 3. Determine which wrap-up code to apply
        # For this example, we assume a fixed queue ID and wrap-up code name
        # In production, you might map this based on the conversation type or queue
        target_queue_id = os.getenv("TARGET_QUEUE_ID")
        target_wrapup_name = os.getenv("DEFAULT_WRAPUP_NAME", "Quality Assurance")
        
        if not target_queue_id:
            raise ValueError("TARGET_QUEUE_ID environment variable not set.")
            
        wrapup_code_id = get_wrapup_code_id(target_queue_id, target_wrapup_name)
        
        # 4. Set the wrap-up code
        wrapup_body = ConversationWrapup(
            wrapup_code_id=wrapup_code_id,
            wrapup_code_name=target_wrapup_name
        )
        
        conversation_api.put_conversations_conversation_wrapup(
            conversation_id=conversation_id,
            body=wrapup_body
        )
        
        print(f"Successfully set wrap-up code for conversation {conversation_id}.")
        
    except ApiException as e:
        # Handle 409 Conflict: Wrap-up already set
        if e.status == 409:
            print(f"Conflict: Wrap-up code already set for conversation {conversation_id}.")
        else:
            print(f"Error setting wrap-up code: {e}")
            raise

OAuth Scope Required: conversation:read, routing:wrapup:write

Step 3: Implementing a Polling Fallback (Optional but Recommended)

Webhooks can occasionally drop messages or experience latency. For critical compliance requirements, a polling mechanism can verify that wrap-up codes are applied within a certain time window. This step uses the Analytics API to query recent conversations that are in the WRAPPED state but lack a wrap-up code.

from genesyscloud import AnalyticsApi
from datetime import datetime, timedelta

def check_unwrapped_conversations() -> None:
    """
    Polls for conversations that are WRAPPED but have no wrap-up code.
    Applies the default wrap-up code if found.
    """
    analytics_api = AnalyticsApi(genesys_config)
    
    # Define the time range: last 5 minutes
    end_time = datetime.utcnow()
    start_time = end_time - timedelta(minutes=5)
    
    # Format dates as ISO 8601
    start_str = start_time.isoformat() + "Z"
    end_str = end_time.isoformat() + "Z"
    
    # Build the query body
    query_body = {
        "interval": f"{start_str}/{end_str}",
        "groupBy": [],
        "filter": {
            "type": "and",
            "clauses": [
                {
                    "type": "equals",
                    "path": "state",
                    "value": "WRAPPED"
                },
                {
                    "type": "equals",
                    "path": "wrapupCode.id",
                    "value": "null"  # Check for null wrap-up code
                }
            ]
        }
    }
    
    try:
        # Query analytics for conversations
        response = analytics_api.post_analytics_conversations_details_query(
            body=query_body,
            limit=100  # Process in batches
        )
        
        if response.entities:
            for entity in response.entities:
                conversation_id = entity.entity_id
                # Reuse the logic from Step 2 to set the wrap-up code
                handle_conversation_state_change({
                    "conversationId": conversation_id,
                    "state": "WRAPPED"
                })
                
        else:
            print("No unwrapped conversations found in the last 5 minutes.")
            
    except ApiException as e:
        print(f"Error querying analytics: {e}")
        raise

OAuth Scope Required: analytics:conversations:read

Complete Working Example

The following script combines the authentication, wrap-up code retrieval, and webhook handling into a single executable module. It assumes you are running this within a web framework (like Flask or FastAPI) that exposes an endpoint for the webhook. For demonstration, we use a simple Flask app.

import os
from flask import Flask, request, jsonify
from genesyscloud import Configuration, ConversationApi
from genesyscloud.models import ConversationWrapup
from genesyscloud.rest import ApiException

app = Flask(__name__)

# Global configuration
genesys_config = Configuration()
genesys_config.oauth_client_id = os.getenv("GENESYS_CLIENT_ID")
genesys_config.oauth_client_secret = os.getenv("GENESYS_CLIENT_SECRET")

def get_wrapup_code_id(queue_id: str, wrapup_code_name: str) -> str:
    from genesyscloud import RoutingApi
    routing_api = RoutingApi(genesys_config)
    try:
        queue = routing_api.get_routing_queue(queue_id=queue_id, expand=["wrapupcodes"])
        if not queue.wrapup_codes:
            raise ValueError("No wrap-up codes found.")
        for code in queue.wrapup_codes:
            if code.name == wrapup_code_name:
                return code.id
        raise ValueError(f"Wrap-up code '{wrapup_code_name}' not found.")
    except ApiException as e:
        raise RuntimeError(f"Failed to get wrap-up code: {e}")

@app.route('/webhook/conversation-state', methods=['POST'])
def webhook_handler():
    """
    Endpoint to receive Genesys Cloud conversation state webhooks.
    """
    # Verify the request is from Genesys Cloud (optional but recommended)
    # You would typically check the signature here
    
    payload = request.get_json()
    if not payload:
        return jsonify({"error": "Invalid JSON"}), 400
        
    conversation_id = payload.get("conversationId")
    state = payload.get("state")
    
    if state != "WRAPPED":
        return jsonify({"status": "ignored"}), 200
        
    conversation_api = ConversationApi(genesys_config)
    
    try:
        # Check if wrap-up is already set
        conversation = conversation_api.get_conversations_conversation(conversation_id=conversation_id)
        
        if conversation.wrapup_code and conversation.wrapup_code.id:
            return jsonify({"status": "already_set"}), 200
            
        # Get wrap-up code ID
        queue_id = os.getenv("TARGET_QUEUE_ID")
        wrapup_name = os.getenv("DEFAULT_WRAPUP_NAME", "Quality Assurance")
        
        wrapup_id = get_wrapup_code_id(queue_id, wrapup_name)
        
        # Set wrap-up code
        wrapup_body = ConversationWrapup(
            wrapup_code_id=wrapup_id,
            wrapup_code_name=wrapup_name
        )
        
        conversation_api.put_conversations_conversation_wrapup(
            conversation_id=conversation_id,
            body=wrapup_body
        )
        
        return jsonify({"status": "success", "conversationId": conversation_id}), 200
        
    except ApiException as e:
        if e.status == 409:
            return jsonify({"status": "conflict", "message": "Wrap-up already set"}), 200
        return jsonify({"error": str(e)}), 500
    except Exception as e:
        return jsonify({"error": str(e)}), 500

if __name__ == '__main__':
    # Run the Flask app
    # In production, use a proper WSGI server like Gunicorn
    app.run(host='0.0.0.0', port=5000)

Common Errors & Debugging

Error: 409 Conflict

  • What causes it: The API returns a 409 Conflict when you attempt to set a wrap-up code on a conversation that already has one. This often happens if the agent manually sets the code before your webhook processes, or if your webhook is triggered multiple times for the same state change.
  • How to fix it: Implement idempotency checks. Before calling PUT /conversations/{conversationId}/wrapup, retrieve the conversation details using GET /conversations/{conversationId}. Check if wrapup_code.id is not null. If it is not null, skip the update.
  • Code showing the fix:
conversation = conversation_api.get_conversations_conversation(conversation_id=conversation_id)
if conversation.wrapup_code and conversation.wrapup_code.id:
    # Wrap-up already exists, do nothing
    pass
else:
    # Proceed to set wrap-up
    conversation_api.put_conversations_conversation_wrapup(...)

Error: 403 Forbidden

  • What causes it: The OAuth token used in the request lacks the required scope. Specifically, setting a wrap-up code requires routing:wrapup:write. Reading the conversation requires conversation:read.
  • How to fix it: Verify that your OAuth Client in the Genesys Cloud Admin Console has both conversation:read and routing:wrapup:write scopes enabled. Ensure you are using the correct client ID and secret.
  • Code showing the fix:
# Ensure your OAuth client configuration includes these scopes
config.oauth_client_id = "your_client_id"
config.oauth_client_secret = "your_client_secret"
# The SDK will automatically request these scopes during token acquisition

Error: 404 Not Found

  • What causes it: The conversationId provided in the webhook payload is invalid, or the wrap-up code ID you are trying to apply does not exist in the specified queue.
  • How to fix it: Validate the conversationId by attempting to fetch it. Ensure the wrap-up code name matches exactly with the one defined in the queue. Use the get_wrapup_code_id function to dynamically resolve the ID.
  • Code showing the fix:
try:
    conversation = conversation_api.get_conversations_conversation(conversation_id=conversation_id)
except ApiException as e:
    if e.status == 404:
        print(f"Conversation {conversation_id} not found.")
    raise

Official References

I normally fix this by using the Analytics API instead of trying to patch terminated conversations.

Cause:
Genesys Cloud locks conversation metadata upon termination. You cannot POST/PUT to /api/v2/conversations/{id} for wrap-ups after TERMINATED state.

Solution:
Use the Analytics Detail query to retrieve the data, or use the Interaction API if you need to update interaction-level data. For pure wrap-up codes, you generally must set them before termination. If you need post-hoc tagging, use the Interaction API’s PATCH /api/v2/interactions/{interactionId} with the wrapupCode field, but note this is for interactions, not conversations.

// Pulumi TS example for creating a wrap-up code definition
import * as genesyscloud from '@genesyscloud/pulumi-provider';

const wrapUpCode = new genesyscloud.wrapupcode.Wrapupcode("myWrapUp", {
 name: "Post-Call Summary",
 code: "POST_SUMMARY",
 description: "Used for post-call summaries",
 enabled: true
});

If you are trying to update an existing interaction’s wrap-up, use the Interaction API. The Conversation API does not support this post-termination.