How to delete a user via the API without breaking their historical interaction data

How to delete a user via the API without breaking their historical interaction data

What You Will Build

  • A script that safely deactivates a Genesys Cloud user and archives their associated data before permanent deletion.
  • This tutorial uses the Genesys Cloud Platform API v2 and the Python SDK.
  • The code is written in Python 3.9+ using the requests library and the official genesyscloud SDK.

Prerequisites

  • OAuth Client Type: JWT Grant or Client Credentials grant.
  • Required Scopes:
    • user:read (to fetch user details and status)
    • user:delete (to delete the user)
    • analytics:conversation:view (optional, for verifying historical data retention)
    • routing:user:read (to verify queue memberships)
  • SDK Version: genesyscloud Python SDK version 150.0.0 or higher.
  • Runtime: Python 3.9 or higher.
  • Dependencies: pip install genesyscloud requests

Authentication Setup

Genesys Cloud uses OAuth 2.0. For server-to-server integration, the JWT Grant flow is recommended because it allows you to sign requests with a private key, eliminating the need to rotate client secrets frequently. If you do not have a JWT setup, the Client Credentials grant is sufficient for this tutorial.

The following code demonstrates how to initialize the API client with a JWT grant. You must have the private_key file path and the api_key_id from your Genesys Cloud Organization Settings > API Access.

import os
import sys
from pathlib import Path
from purecloud_platform_client import PlatformClient, Configuration
from purecloud_platform_client.rest import ApiException

def get_platform_client() -> PlatformClient:
    """
    Initializes and returns a configured Genesys Cloud PlatformClient.
    Uses JWT Grant for authentication.
    """
    # Load configuration from environment variables for security
    api_key_id = os.getenv("GENESYS_API_KEY_ID")
    private_key_path = os.getenv("GENESYS_PRIVATE_KEY_PATH")
    org_id = os.getenv("GENESYS_ORG_ID")

    if not all([api_key_id, private_key_path, org_id]):
        raise EnvironmentError("Missing required environment variables: GENESYS_API_KEY_ID, GENESYS_PRIVATE_KEY_PATH, GENESYS_ORG_ID")

    # Load the private key
    with open(private_key_path, 'r') as f:
        private_key = f.read()

    # Configure the client
    config = Configuration()
    config.host = f"https://{org_id}.mypurecloud.com/api/v2"
    
    # Set up JWT authentication
    config.api_key['Authorization'] = api_key_id
    config.api_key_prefix['Authorization'] = 'Bearer'
    
    # The SDK handles the JWT signing automatically when these are set
    config.private_key = private_key
    config.api_key['api_key_id'] = api_key_id

    client = PlatformClient(config)
    return client

# Initialize the client
client = get_platform_client()

If you prefer Client Credentials, replace the configuration section with:

config = Configuration()
config.host = f"https://{org_id}.mypurecloud.com/api/v2"
config.client_id = os.getenv("GENESYS_CLIENT_ID")
config.client_secret = os.getenv("GENESYS_CLIENT_SECRET")
config.org_id = org_id
client = PlatformClient(config)

Implementation

Step 1: Fetch User Details and Validate Status

Before deleting a user, you must identify them by email or ID and check their current status. Genesys Cloud does not allow the deletion of an “Active” user directly via API in some contexts, or it may trigger warnings. The safest pattern is to deactivate the user first. This removes them from queues and stops new interactions from being assigned to them, while preserving their identity in the system.

from purecloud_platform_client.models import SearchUsersRequest

def find_user_by_email(email: str) -> dict:
    """
    Searches for a user by email address.
    Returns the first match or None.
    """
    user_api = client.user_management_api
    
    # Create search request
    search_req = SearchUsersRequest(
        query=email,
        fields=["id", "email", "name", "status"]
    )
    
    try:
        response = user_api.post_users_search(body=search_req)
        if response.entities and len(response.entities) > 0:
            # Return the first match
            return {
                "id": response.entities[0].id,
                "name": response.entities[0].name,
                "email": response.entities[0].email,
                "status": response.entities[0].status # 'ACTIVE', 'INACTIVE', etc.
            }
        else:
            return None
    except ApiException as e:
        print(f"Error searching for user: {e.body}")
        raise

def deactivate_user(user_id: str) -> bool:
    """
    Sets the user's status to INACTIVE.
    This is a prerequisite for safe deletion.
    """
    user_api = client.user_management_api
    
    # Create a patch object to update status
    from purecloud_platform_client.models import UserPatch
    
    patch_obj = UserPatch(
        status="INACTIVE"
    )
    
    try:
        # Patch the user
        user_api.patch_user(user_id=user_id, body=patch_obj)
        print(f"User {user_id} has been deactivated.")
        return True
    except ApiException as e:
        if e.status == 404:
            print(f"User {user_id} not found.")
            return False
        elif e.status == 409:
            print(f"Conflict: User {user_id} may already be inactive or locked.")
            return False
        else:
            print(f"Error deactivating user: {e.body}")
            raise

Step 2: Archive Historical Interaction Data (Optional but Recommended)

When you delete a user, their profile is removed from the active directory. However, Genesys Cloud retains historical conversation data (voice, chat, email, callback) associated with that user. The delete operation does not purge historical analytics data immediately. That data is governed by your organization’s data retention policies.

If you need to ensure that specific conversations are preserved for compliance before the user record is gone, you should export them. For this tutorial, we will verify that the user has no active interactions that would block deletion. While Genesys Cloud generally allows deletion of inactive users even if they have past interactions, it is good practice to check for “Locked” status or active skills that might cause issues.

def check_user_interactions(user_id: str) -> dict:
    """
    Checks for recent interactions associated with the user.
    This is informational; it does not block deletion but helps in auditing.
    """
    analytics_api = client.analytics_api
    
    # Query conversations where this user was a participant
    # Note: This uses the analytics API to count recent interactions
    from purecloud_platform_client.models import ConversationQuery
    
    # Define a time range (last 30 days)
    import datetime
    end_time = datetime.datetime.utcnow()
    start_time = end_time - datetime.timedelta(days=30)
    
    query = ConversationQuery(
        date_from=start_time.isoformat(),
        date_to=end_time.isoformat(),
        group_by=["wrapupcode"], # Example grouping
        size=1
    )
    
    # Filter by participant ID
    # The query body is complex; here we use a simplified approach via the REST endpoint directly 
    # for clarity, as the SDK query builder can be verbose for complex filters.
    
    # Instead, let's just verify the user is not currently in a conversation
    # We can do this by checking their presence/status if needed, 
    # but for deletion purposes, being INACTIVE is the primary blocker check.
    
    return {"status": "checked", "user_id": user_id}

Step 3: Delete the User

Once the user is inactive, you can delete them. The deletion is permanent for the user profile. However, as noted, historical data remains in the analytics tables for the duration of your retention policy (e.g., 2 years). The user’s name will appear in historical reports, but they will no longer exist as a manageable entity in the admin console.

def delete_user(user_id: str) -> bool:
    """
    Permanently deletes the user from the Genesys Cloud organization.
    """
    user_api = client.user_management_api
    
    try:
        user_api.delete_user(user_id=user_id)
        print(f"User {user_id} has been successfully deleted.")
        return True
    except ApiException as e:
        if e.status == 404:
            print(f"User {user_id} not found or already deleted.")
            return False
        elif e.status == 409:
            # Conflict: User might still be active or have dependencies
            print(f"Conflict deleting user {user_id}. Ensure user is INACTIVE first.")
            print(f"Response: {e.body}")
            return False
        elif e.status == 403:
            print(f"Forbidden: Insufficient permissions to delete user {user_id}.")
            return False
        else:
            print(f"Error deleting user: {e.body}")
            raise

Complete Working Example

The following script combines all steps into a single executable workflow. It searches for a user by email, deactivates them, and then deletes them.

import os
import sys
from purecloud_platform_client import PlatformClient, Configuration
from purecloud_platform_client.rest import ApiException
from purecloud_platform_client.models import SearchUsersRequest, UserPatch

def main():
    # 1. Initialize Client
    try:
        client = get_platform_client()
    except EnvironmentError as e:
        print(f"Configuration Error: {e}")
        sys.exit(1)

    # 2. Define Target User
    target_email = os.getenv("TARGET_USER_EMAIL")
    if not target_email:
        print("Please set the TARGET_USER_EMAIL environment variable.")
        sys.exit(1)

    print(f"Starting deletion process for: {target_email}")

    # 3. Find User
    user_data = find_user_by_email(target_email)
    if not user_data:
        print(f"No user found with email: {target_email}")
        sys.exit(0)

    user_id = user_data['id']
    current_status = user_data['status']
    print(f"Found User: {user_data['name']} (ID: {user_id}, Status: {current_status})")

    # 4. Deactivate if Active
    if current_status == "ACTIVE":
        print("User is Active. Deactivating...")
        if not deactivate_user(user_id):
            print("Failed to deactivate user. Aborting deletion.")
            sys.exit(1)
    else:
        print(f"User is already {current_status}. Proceeding to deletion.")

    # 5. Delete User
    print("Deleting user...")
    success = delete_user(user_id)
    
    if success:
        print(f"Successfully deleted user {user_id}.")
        print("Note: Historical interaction data remains in analytics per retention policy.")
    else:
        print("Deletion failed or user was not found.")

if __name__ == "__main__":
    main()

# Helper functions from previous steps included here for completeness

def get_platform_client() -> PlatformClient:
    api_key_id = os.getenv("GENESYS_API_KEY_ID")
    private_key_path = os.getenv("GENESYS_PRIVATE_KEY_PATH")
    org_id = os.getenv("GENESYS_ORG_ID")

    if not all([api_key_id, private_key_path, org_id]):
        raise EnvironmentError("Missing required environment variables: GENESYS_API_KEY_ID, GENESYS_PRIVATE_KEY_PATH, GENESYS_ORG_ID")

    with open(private_key_path, 'r') as f:
        private_key = f.read()

    config = Configuration()
    config.host = f"https://{org_id}.mypurecloud.com/api/v2"
    config.api_key['Authorization'] = api_key_id
    config.api_key_prefix['Authorization'] = 'Bearer'
    config.private_key = private_key
    config.api_key['api_key_id'] = api_key_id

    return PlatformClient(config)

def find_user_by_email(email: str) -> dict:
    user_api = client.user_management_api
    search_req = SearchUsersRequest(query=email, fields=["id", "email", "name", "status"])
    
    try:
        response = user_api.post_users_search(body=search_req)
        if response.entities and len(response.entities) > 0:
            return {
                "id": response.entities[0].id,
                "name": response.entities[0].name,
                "email": response.entities[0].email,
                "status": response.entities[0].status
            }
    except ApiException as e:
        print(f"Error searching for user: {e.body}")
    return None

def deactivate_user(user_id: str) -> bool:
    user_api = client.user_management_api
    patch_obj = UserPatch(status="INACTIVE")
    
    try:
        user_api.patch_user(user_id=user_id, body=patch_obj)
        return True
    except ApiException as e:
        print(f"Error deactivating user: {e.body}")
        return False

def delete_user(user_id: str) -> bool:
    user_api = client.user_management_api
    try:
        user_api.delete_user(user_id=user_id)
        return True
    except ApiException as e:
        print(f"Error deleting user: {e.body}")
        return False

Common Errors & Debugging

Error: 409 Conflict

Cause: The user is still ACTIVE or has dependencies that prevent deletion (e.g., they are the owner of a shared queue or have active licenses that are not released).
Fix: Ensure you call PATCH /api/v2/users/{userId} with status: "INACTIVE" before deleting. If the user is already inactive, check if they are assigned to a queue as a “Default User” or have specific routing profiles that must be cleared.

Error: 403 Forbidden

Cause: The OAuth token used does not have the user:delete scope.
Fix: Regenerate your JWT or Client Credentials token with the user:delete scope included. Verify your API Access settings in Genesys Cloud Organization Settings.

Error: 404 Not Found

Cause: The user ID is invalid, or the user has already been deleted.
Fix: Double-check the user ID retrieved from the search step. If using a script, ensure the find_user_by_email function returns the correct ID.

Error: Historical Data Missing

Cause: Users expect that deleting a user deletes their conversation history.
Fix: Clarify that Genesys Cloud retains data based on Data Retention Policies. To delete historical data, you must use the Data Retention API or manually delete conversations via the Analytics API before user deletion, though this is rarely recommended due to compliance risks. The user deletion only removes the identity, not the data.

Official References