Delete a Genesys Cloud User Without Losing Historical Data

Delete a Genesys Cloud User Without Losing Historical Data

What You Will Build

  • A script that safely deactivates a Genesys Cloud user and removes their active routing status, ensuring all historical conversation data remains queryable and attributed correctly.
  • This tutorial uses the Genesys Cloud Platform API (v2) and the Python SDK (genesyscloud).
  • The programming language covered is Python 3.9+.

Prerequisites

  • OAuth Client Type: Service Account (Client Credentials Grant).
  • Required Scopes:
    • user:read (to verify user existence and status)
    • user:write (to update user status)
    • routing:write (to update wrap-up codes or routing profiles if needed, though primarily for status changes)
    • analytics:conversations:read (optional, for verification steps)
  • SDK Version: genesyscloud >= 140.0.0 (Ensure you are using a recent version to support the latest status field behaviors).
  • Language/Runtime: Python 3.9 or higher.
  • External Dependencies:
    • genesyscloud
    • python-dotenv (for secure credential management)

Authentication Setup

Genesys Cloud uses OAuth 2.0 for API authentication. For server-to-server integrations like this user management script, the Client Credentials Grant flow is the standard. This flow requires a Service Account with the necessary permissions assigned.

Do not hardcode credentials. Use environment variables.

import os
from dotenv import load_dotenv
from genesyscloud import PlatformClient
from genesyscloud.auth_api_client import AuthApiClient

# Load environment variables from .env file
load_dotenv()

def get_platform_client() -> PlatformClient:
    """
    Initializes and returns an authenticated PlatformClient.
    """
    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 must be set in environment.")

    client = PlatformClient(base_url=base_url)
    
    # Authenticate using Client Credentials Grant
    try:
        client.authenticate_client_credentials(client_id, client_secret)
        return client
    except Exception as e:
        print(f"Authentication failed: {e}")
        raise e

# Get the initialized client
platform_client = get_platform_client()

Implementation

Step 1: Locate the User and Validate Current Status

Before modifying a user, you must retrieve their unique UUID and current status. Genesys Cloud identifies users by UUID, not by email. Email addresses can change or be reused, making them unreliable for API targeting.

The critical distinction in Genesys Cloud is between Deactivation and Deletion.

  • Deactivation (status: "inactive"): The user record remains. Historical data is preserved. The user cannot log in. This is the safe, recommended approach for compliance and data integrity.
  • Deletion: Genesys Cloud does not allow true “hard deletion” of user records via API to preserve audit trails and data integrity. You can only deactivate.
from genesyscloud import UsersApi
from genesyscloud.rest import ApiException
from typing import Optional

users_api = UsersApi(platform_client)

def find_user_by_email(email: str) -> Optional[str]:
    """
    Searches for a user by email address and returns their UUID.
    
    Args:
        email: The email address of the user to find.
        
    Returns:
        The user UUID if found, None otherwise.
    """
    try:
        # Query users by email. Note: This may return multiple results if emails are not unique
        # (though rare in proper setups). We assume the first match for this tutorial.
        response = users_api.post_users_query(
            body={
                "emails": [email],
                "status": "active"  # Only look for active users to avoid modifying already inactive ones
            }
        )
        
        if response.users and len(response.users) > 0:
            target_user = response.users[0]
            print(f"Found User: {target_user.name} (UUID: {target_user.id})")
            return target_user.id
        else:
            print(f"No active user found with email: {email}")
            return None

    except ApiException as e:
        if e.status == 401:
            print("Authentication error: Token may be expired or invalid.")
        elif e.status == 403:
            print("Permission error: Check if the service account has 'user:read' scope.")
        else:
            print(f"API Error {e.status}: {e.reason}")
        return None

Step 2: Deactivate the User (The “Safe Delete”)

To “delete” a user without breaking historical data, you must change their status to inactive. This action:

  1. Prevents the user from logging in.
  2. Removes them from active routing queues (depending on specific queue settings, but generally they stop receiving new work).
  3. Preserves all past interactions (calls, chats, messages) associated with their UUID.
  4. Ensures analytics reports (e.g., “Agent Performance”) can still attribute past work to this agent.

If you simply remove a user from a team or change their role without deactivating, they remain an active license consumer and can still log in.

from genesyscloud import UsersApi
from genesyscloud.rest import ApiException

def deactivate_user(user_id: str) -> bool:
    """
    Deactivates a user by setting their status to 'inactive'.
    
    Args:
        user_id: The UUID of the user to deactivate.
        
    Returns:
        True if successful, False otherwise.
    """
    try:
        # Construct the update payload
        # Note: We only send the fields we want to change. 
        # Sending other fields might overwrite existing configurations if not careful.
        update_payload = {
            "status": "inactive"
        }
        
        # PATCH request to update the user
        # The SDK method is patch_user
        users_api.patch_user(
            user_id=user_id,
            body=update_payload
        )
        
        print(f"Successfully deactivated user {user_id}.")
        return True

    except ApiException as e:
        if e.status == 404:
            print(f"User {user_id} not found. It may have already been deleted or deactivated.")
        elif e.status == 409:
            # Conflict: Often occurs if the user is currently in a call or has active sessions
            print(f"Conflict: User {user_id} may be in an active session. Wait for session to end and retry.")
        elif e.status == 403:
            print("Permission error: Check if the service account has 'user:write' scope.")
        else:
            print(f"API Error {e.status}: {e.reason}")
        return False

Step 3: Verify Historical Data Integrity

After deactivation, it is critical to verify that historical data is still accessible. The most robust way to do this is to query the Analytics API for conversations associated with that user ID. If the user was “hard deleted” (which is impossible via API, but hypothetically), these records might become orphaned or unattributable. With deactivation, they remain intact.

from genesyscloud import AnalyticsApi
from datetime import datetime, timedelta

analytics_api = AnalyticsApi(platform_client)

def verify_historical_data(user_id: str, days_back: int = 30) -> bool:
    """
    Queries analytics for conversations handled by the user in the last N days.
    
    Args:
        user_id: The UUID of the user.
        days_back: Number of days to look back for data.
        
    Returns:
        True if data is found and accessible, False otherwise.
    """
    # Define the time range
    end_time = datetime.utcnow()
    start_time = end_time - timedelta(days=days_back)
    
    # Format times for API (ISO 8601)
    time_format = "%Y-%m-%dT%H:%M:%S.000Z"
    query_body = {
        "dateRange": {
            "startTime": start_time.strftime(time_format),
            "endTime": end_time.strftime(time_format)
        },
        "filters": {
            "to": [
                {
                    "dimension": "user.id",
                    "type": "constant",
                    "values": [user_id]
                }
            ]
        },
        "metrics": [
            "conv"  # Count of conversations
        ],
        "groupBy": [],
        "size": 100
    }
    
    try:
        # Post analytics conversations details query
        response = analytics_api.post_analytics_conversations_details_query(body=query_body)
        
        # Check if any results were returned
        if response.entities and len(response.entities) > 0:
            total_conversations = sum([entity.metrics['conv'] for entity in response.entities])
            print(f"Verification: Found {total_conversations} historical conversations for user {user_id}.")
            print("Historical data is intact and attributable.")
            return True
        else:
            print(f"No historical conversations found in the last {days_back} days. Data may be empty, but attribution is intact.")
            return True

    except ApiException as e:
        if e.status == 403:
            print("Permission error: Check if the service account has 'analytics:conversations:read' scope.")
        else:
            print(f"Analytics API Error {e.status}: {e.reason}")
        return False

Complete Working Example

This script combines the steps above into a single executable module. It handles authentication, user lookup, deactivation, and verification.

import os
import sys
from dotenv import load_dotenv
from genesyscloud import PlatformClient, UsersApi, AnalyticsApi
from genesyscloud.rest import ApiException
from datetime import datetime, timedelta
from typing import Optional

# Load environment variables
load_dotenv()

def get_platform_client() -> PlatformClient:
    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 must be set.")

    client = PlatformClient(base_url=base_url)
    try:
        client.authenticate_client_credentials(client_id, client_secret)
        return client
    except Exception as e:
        print(f"Authentication failed: {e}")
        sys.exit(1)

def find_user_by_email(users_api: UsersApi, email: str) -> Optional[str]:
    try:
        response = users_api.post_users_query(
            body={
                "emails": [email],
                "status": "active"
            }
        )
        if response.users and len(response.users) > 0:
            return response.users[0].id
        return None
    except ApiException as e:
        print(f"Error finding user: {e.reason}")
        return None

def deactivate_user(users_api: UsersApi, user_id: str) -> bool:
    try:
        users_api.patch_user(
            user_id=user_id,
            body={"status": "inactive"}
        )
        print(f"User {user_id} deactivated successfully.")
        return True
    except ApiException as e:
        if e.status == 409:
            print(f"User {user_id} is in an active session. Please wait.")
        else:
            print(f"Error deactivating user: {e.reason}")
        return False

def verify_history(analytics_api: AnalyticsApi, user_id: str) -> bool:
    end_time = datetime.utcnow()
    start_time = end_time - timedelta(days=30)
    time_format = "%Y-%m-%dT%H:%M:%S.000Z"
    
    query_body = {
        "dateRange": {
            "startTime": start_time.strftime(time_format),
            "endTime": end_time.strftime(time_format)
        },
        "filters": {
            "to": [
                {
                    "dimension": "user.id",
                    "type": "constant",
                    "values": [user_id]
                }
            ]
        },
        "metrics": ["conv"],
        "groupBy": [],
        "size": 1
    }
    
    try:
        analytics_api.post_analytics_conversations_details_query(body=query_body)
        print("Historical data verification passed (API call succeeded).")
        return True
    except ApiException as e:
        print(f"Verification failed: {e.reason}")
        return False

def main():
    if len(sys.argv) < 2:
        print("Usage: python deactivate_user.py <email_address>")
        sys.exit(1)
        
    target_email = sys.argv[1]
    print(f"Starting deactivation process for: {target_email}")
    
    client = get_platform_client()
    users_api = UsersApi(client)
    analytics_api = AnalyticsApi(client)
    
    # Step 1: Find User
    user_id = find_user_by_email(users_api, target_email)
    if not user_id:
        print("User not found or already inactive. Exiting.")
        return
    
    # Step 2: Deactivate
    if deactivate_user(users_api, user_id):
        # Step 3: Verify
        verify_history(analytics_api, user_id)
    else:
        print("Deactivation failed.")

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 409 Conflict

  • What causes it: The user is currently in an active conversation (voice, chat, or message). Genesys Cloud prevents status changes during active sessions to prevent data corruption or agent confusion.
  • How to fix it: Implement a retry loop with a delay. Check the user’s current status or wait for the session to end.
  • Code showing the fix:
    import time
    
    def deactivate_with_retry(users_api: UsersApi, user_id: str, max_retries: int = 3, delay: int = 60) -> bool:
        for attempt in range(max_retries):
            try:
                users_api.patch_user(user_id=user_id, body={"status": "inactive"})
                return True
            except ApiException as e:
                if e.status == 409:
                    if attempt < max_retries - 1:
                        print(f"User in session. Retrying in {delay} seconds... (Attempt {attempt + 1})")
                        time.sleep(delay)
                    else:
                        print("Max retries reached. User is still in a session.")
                        return False
                else:
                    raise e
        return False
    

Error: 403 Forbidden

  • What causes it: The Service Account used for authentication lacks the user:write scope.
  • How to fix it: Go to the Genesys Cloud Admin Console > Admin > Security > API Access > Service Accounts. Edit the specific service account and add the user:write permission to its role.

Error: 404 Not Found

  • What causes it: The user UUID is invalid, or the user has already been deactivated/deleted.
  • How to fix it: Verify the UUID using the POST /api/v2/users/query endpoint again without the status: active filter to see if the user exists in an inactive state.

Official References