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 and removes a user from Genesys Cloud CX while preserving the integrity of their historical conversation records.
  • This tutorial uses the Genesys Cloud CX REST API and Python SDK.
  • The implementation is written in Python 3.9+ using the genesyscloud SDK and requests for fallback operations.

Prerequisites

  • OAuth Client Type: Service Account or JWT (JSON Web Token) authentication is required. Client Credentials flow is also acceptable if the client has the necessary scopes.
  • Required Scopes:
    • user:read (to verify user existence and status)
    • user:write (to deactivate the user)
    • user:delete (to permanently remove the user, if applicable)
    • conversation:read (to verify historical data integrity, optional but recommended)
  • SDK Version: Genesys Cloud Python SDK v2 (latest stable).
  • Language/Runtime: Python 3.9 or higher.
  • External Dependencies:
    • genesyscloud
    • requests
    • python-dotenv (for secure credential management)

Authentication Setup

Genesys Cloud CX uses OAuth 2.0. For API-driven user management, a Service Account or JWT is preferred because it does not require interactive login and can run in background processes.

First, install the required packages:

pip install genesyscloud requests python-dotenv

Create a .env file to store your credentials securely:

GENESYS_CLOUD_REGION=us-east-1
GENESYS_CLOUD_CLIENT_ID=your_client_id
GENESYS_CLOUD_CLIENT_SECRET=your_client_secret
GENESYS_CLOUD_JWT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----...

Here is the authentication logic using the Genesys Cloud Python SDK. The SDK handles token caching and refresh automatically.

import os
from dotenv import load_dotenv
from genesyscloud.rest import Configuration
from genesyscloud.api_client import ApiClient
from genesyscloud.users_api import UsersApi
from genesyscloud.conversations_api import ConversationsApi
from genesyscloud.analytics_api import AnalyticsApi
import time

load_dotenv()

def get_api_client():
    """
    Initializes and returns the Genesys Cloud API client.
    Uses JWT authentication by default. Falls back to Client Credentials if JWT key is not present.
    """
    region = os.getenv("GENESYS_CLOUD_REGION")
    client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
    private_key = os.getenv("GENESYS_CLOUD_JWT_PRIVATE_KEY")

    config = Configuration(
        host=f"https://{region}.mypurecloud.com",
        client_id=client_id,
        client_secret=client_secret,
        jwt_private_key=private_key
    )

    api_client = ApiClient(configuration=config)
    return api_client

# Initialize APIs
api_client = get_api_client()
users_api = UsersApi(api_client)
conversations_api = ConversationsApi(api_client)

Implementation

Step 1: Verify User Existence and Status

Before attempting to delete or deactivate a user, you must verify that the user exists and check their current status. Deleting an already deleted user will return a 404. Activating a deactivated user is different from deleting one.

The critical distinction in Genesys Cloud is between Deactivating and Deleting.

  • Deactivating (PUT /api/v2/users/{id} with status: "inactive"): The user remains in the system. Historical data (chats, emails, tasks) retains the link to this user ID. This is the safe way to “remove” a user without breaking history.
  • Deleting (DELETE /api/v2/users/{id}): The user is permanently removed. Genesys Cloud does not delete historical interactions associated with the user. The to or from fields in historical data will still reference the user ID, but the user profile will return 404. This can break UI components that try to resolve the user name from the ID.

Best Practice: Always deactivate first. Only delete if you are certain no internal tools rely on resolving the user profile for historical reports.

def get_user_by_email(email: str):
    """
    Retrieves a user object by their email address.
    """
    try:
        # Search for users by email
        response = users_api.post_users_search(
            body={
                "query": email,
                "searchCriteria": ["email"]
            }
        )
        
        if response.entities and len(response.entities) > 0:
            return response.entities[0]
        else:
            return None
    except Exception as e:
        print(f"Error searching for user: {e}")
        return None

def check_user_status(user_id: str):
    """
    Gets the detailed user object to check status and active interactions.
    """
    try:
        user = users_api.get_user_by_id(user_id=user_id)
        return user
    except Exception as e:
        if e.status == 404:
            print(f"User {user_id} not found.")
        else:
            print(f"Error fetching user details: {e}")
        return None

Step 2: Check for Active Interactions

You cannot deactivate a user who has active conversations. The API will return a 409 Conflict if you attempt to deactivate a user with ongoing chats, calls, or tasks. You must check for active interactions first.

def has_active_conversations(user_id: str):
    """
    Checks if the user has any active conversations.
    Returns True if active conversations exist, False otherwise.
    """
    try:
        # Query for active conversations involving this user
        # Note: The analytics API is the most reliable way to find active sessions
        # However, a simpler check is often sufficient via the users API which 
        # sometimes includes active session counts, but let's use the conversations API.
        
        # We construct a query for active conversations where this user is a participant
        body = {
            "view": "realtime",
            "dateFrom": "2023-01-01T00:00:00.000Z", # Arbitrary past date
            "dateTo": "2099-01-01T00:00:00.000Z",   # Arbitrary future date
            "query": {
                "predicates": [
                    {
                        "type": "equals",
                        "field": "participants.userId",
                        "value": user_id
                    },
                    {
                        "type": "equals",
                        "field": "state",
                        "value": "active"
                    }
                ]
            },
            "size": 1
        }
        
        response = conversations_api.post_conversations_details_query(
            body=body
        )
        
        if response.entities and len(response.entities) > 0:
            return True
        return False
        
    except Exception as e:
        print(f"Error checking active conversations: {e}")
        return False # Assume false if error, but handle gracefully

Step 3: Deactivate the User (Safe Removal)

Deactivating the user is the standard procedure for “removing” a user while keeping their historical data intact. The user ID remains valid in historical records.

def deactivate_user(user_id: str):
    """
    Deactivates the user by setting their status to 'inactive'.
    This preserves historical data links.
    """
    try:
        # Update user status to inactive
        users_api.update_user(
            user_id=user_id,
            body={
                "status": "inactive"
            }
        )
        print(f"Successfully deactivated user {user_id}")
        return True
    except Exception as e:
        if e.status == 409:
            print(f"Cannot deactivate user {user_id}: They have active conversations.")
        elif e.status == 400:
            print(f"Bad request: {e.body}")
        else:
            print(f"Error deactivating user: {e}")
        return False

Step 4: Permanently Delete the User (Optional)

If you must permanently remove the user (e.g., GDPR “Right to be Forgotten” where you also want to remove the identity from the directory, though data retention policies may still keep the interaction logs), use the DELETE endpoint.

Warning: This breaks UI lookups. If a dashboard tries to display “Handled By: John Doe” for a chat from 2023, it will fail to resolve “John Doe” and may show “Unknown” or the raw UUID.

def delete_user(user_id: str):
    """
    Permanently deletes the user.
    Use with caution. Historical interactions remain but the user profile is gone.
    """
    try:
        users_api.delete_user(user_id=user_id)
        print(f"Successfully deleted user {user_id}")
        return True
    except Exception as e:
        if e.status == 404:
            print(f"User {user_id} already deleted.")
        else:
            print(f"Error deleting user: {e}")
        return False

Complete Working Example

This script combines all steps into a single workflow. It searches for a user by email, checks for active conversations, deactivates them, and optionally deletes them.

import os
import sys
from dotenv import load_dotenv
from genesyscloud.rest import Configuration
from genesyscloud.api_client import ApiClient
from genesyscloud.users_api import UsersApi
from genesyscloud.conversations_api import ConversationsApi

load_dotenv()

def initialize_api():
    region = os.getenv("GENESYS_CLOUD_REGION")
    client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
    private_key = os.getenv("GENESYS_CLOUD_JWT_PRIVATE_KEY")

    if not all([region, client_id, client_secret]):
        raise ValueError("Missing required environment variables.")

    config = Configuration(
        host=f"https://{region}.mypurecloud.com",
        client_id=client_id,
        client_secret=client_secret,
        jwt_private_key=private_key
    )
    api_client = ApiClient(configuration=config)
    return UsersApi(api_client), ConversationsApi(api_client)

def find_user_by_email(users_api, email):
    try:
        response = users_api.post_users_search(
            body={
                "query": email,
                "searchCriteria": ["email"]
            }
        )
        if response.entities:
            return response.entities[0]
    except Exception as e:
        print(f"Search failed: {e}")
    return None

def check_active_conversations(conversations_api, user_id):
    try:
        body = {
            "view": "realtime",
            "dateFrom": "2023-01-01T00:00:00.000Z",
            "dateTo": "2099-01-01T00:00:00.000Z",
            "query": {
                "predicates": [
                    {"type": "equals", "field": "participants.userId", "value": user_id},
                    {"type": "equals", "field": "state", "value": "active"}
                ]
            },
            "size": 1
        }
        response = conversations_api.post_conversations_details_query(body=body)
        return bool(response.entities)
    except Exception as e:
        print(f"Error checking conversations: {e}")
        return False

def deactivate_and_delete_user(email, delete_permanently=False):
    users_api, conversations_api = initialize_api()
    
    # Step 1: Find User
    user = find_user_by_email(users_api, email)
    if not user:
        print(f"User with email {email} not found.")
        return

    user_id = user.id
    user_name = user.name
    print(f"Found user: {user_name} (ID: {user_id})")
    print(f"Current status: {user.status}")

    # Step 2: Check Active Conversations
    if check_active_conversations(conversations_api, user_id):
        print(f"User {user_name} has active conversations. Cannot deactivate.")
        return

    # Step 3: Deactivate
    if user.status != "inactive":
        try:
            users_api.update_user(user_id=user_id, body={"status": "inactive"})
            print(f"User {user_name} deactivated.")
        except Exception as e:
            print(f"Failed to deactivate user: {e}")
            return
    else:
        print(f"User {user_name} is already inactive.")

    # Step 4: Delete (Optional)
    if delete_permanently:
        try:
            users_api.delete_user(user_id=user_id)
            print(f"User {user_name} permanently deleted.")
        except Exception as e:
            print(f"Failed to delete user: {e}")
    else:
        print(f"User {user_name} remains in system as inactive. History preserved.")

if __name__ == "__main__":
    target_email = os.getenv("TARGET_USER_EMAIL", "[email protected]")
    delete_flag = os.getenv("DELETE_PERMANENTLY", "false").lower() == "true"
    deactivate_and_delete_user(target_email, delete_flag)

Common Errors & Debugging

Error: 409 Conflict - User has active conversations

What causes it:
The user is currently engaged in a chat, call, or has an assigned task that is in an “active” state. Genesys Cloud prevents deactivation to ensure conversation continuity.

How to fix it:

  1. Wait for the conversation to end.
  2. Transfer the active conversation to another user.
  3. If this is an automated process, implement a retry loop with exponential backoff.

Code showing the fix:

import time

def deactivate_with_retry(users_api, user_id, max_retries=3, delay=60):
    for attempt in range(max_retries):
        try:
            users_api.update_user(user_id=user_id, body={"status": "inactive"})
            return True
        except Exception as e:
            if e.status == 409:
                print(f"Attempt {attempt + 1}: User has active conversations. Waiting {delay}s...")
                time.sleep(delay)
            else:
                raise e
    print("Max retries reached. User still has active conversations.")
    return False

Error: 404 Not Found - User does not exist

What causes it:
The user ID or email provided does not match any user in the organization. This can happen if the user was already deleted.

How to fix it:
Verify the email address. Use the post_users_search endpoint to find the correct ID before attempting deletion.

Error: 403 Forbidden - Insufficient Permissions

What causes it:
The OAuth token used does not have the user:write or user:delete scope.

How to fix it:

  1. Go to the Genesys Cloud Admin Console.
  2. Navigate to Admin > Security > OAuth Clients.
  3. Edit your client.
  4. Ensure User > User > Write and Delete scopes are checked.
  5. Regenerate the token.

Error: 429 Too Many Requests

What causes it:
You are making too many API calls in a short period. Genesys Cloud enforces rate limits per client ID.

How to fix it:
Implement rate limiting in your code. Respect the Retry-After header in the response.

Code showing the fix:

import time

def safe_api_call(func, *args, **kwargs):
    max_retries = 5
    for attempt in range(max_retries):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            if e.status == 429:
                retry_after = int(e.headers.get('Retry-After', 2 ** attempt))
                print(f"Rate limited. Waiting {retry_after}s...")
                time.sleep(retry_after)
            else:
                raise e
    raise Exception("Max retries exceeded for rate limiting.")

Official References