Delete a Genesys Cloud User Without Orphaning Historical Data
What You Will Build
- You will write a script that safely deactivates a user in Genesys Cloud while preserving their association with historical interactions, avoiding data integrity violations.
- This tutorial uses the Genesys Cloud PureCloud Platform API and the Python SDK (
genesys-cloud-purecloud-platform-client). - The implementation is written in Python 3.9+, utilizing the
requestslibrary for raw HTTP fallbacks where SDK limitations exist.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Flow) or JWT Bearer Token Flow.
- Required Scopes:
user:read,user:write,user:delete(Note:user:deleteis rarely used directly; we will useuser:writeto deactivate, which is the standard safe practice). - SDK Version:
genesys-cloud-purecloud-platform-client>= 170.0.0. - Runtime: Python 3.9 or higher.
- Dependencies:
pip install genesys-cloud-purecloud-platform-client python-dotenv requests
Authentication Setup
Genesys Cloud uses OAuth 2.0. For server-side scripts, the Client Credentials flow is the most robust method. It requires a Client ID and Client Secret.
Critical Warning: Never hardcode credentials. Use environment variables.
Step 1: Configure Environment Variables
Create a .env file in your project root:
GENESYS_DOMAIN=api.mypurecloud.com
GENESYS_CLIENT_ID=your_client_id_here
GENESYS_CLIENT_SECRET=your_client_secret_here
Step 2: Initialize the SDK Client
The Python SDK handles token acquisition and refresh automatically if configured correctly. However, understanding the underlying HTTP call helps when debugging 401 errors.
import os
from dotenv import load_dotenv
from purecloud_platform_client import (
Configuration,
PureCloudAuthFlow,
UserManagementApi,
UserManagementApiException
)
# Load environment variables
load_dotenv()
def get_auth_configuration() -> Configuration:
"""
Configures the Genesys Cloud SDK with OAuth Client Credentials.
"""
config = Configuration()
config.host = os.getenv("GENESYS_DOMAIN", "api.mypurecloud.com")
config.access_token_url = f"https://{os.getenv('GENESYS_DOMAIN', 'api.mypurecloud.com')}/oauth/token"
config.client_id = os.getenv("GENESYS_CLIENT_ID")
config.client_secret = os.getenv("GENESYS_CLIENT_SECRET")
config.auth_flow = PureCloudAuthFlow.CLIENT_CREDENTIALS
# Optional: Set timeout for long-running operations
config.connection_pool_maxsize = 10
config.connection_pool_timeout = 30
return config
# Initialize the API client
config = get_auth_configuration()
user_api = UserManagementApi(configuration=config)
Implementation
The core misconception in deleting users is the desire to use DELETE /api/v2/users/{userId}. In Genesys Cloud, this endpoint is heavily restricted. If a user has historical data (calls, chats, emails, tickets) associated with their ID, a hard delete often fails with a 409 Conflict or results in data that is no longer attributable to a specific identity, breaking reporting.
The industry-standard practice is Deactivation. This sets the user’s status to inactive. The user ID remains in the database, historical interactions remain linked to that ID, but the user can no longer log in or be assigned new work.
Step 1: Retrieve User Details and Current Status
Before modifying a user, you must verify their current state. Attempting to deactivate an already inactive user is idempotent but generates unnecessary API calls.
Endpoint: GET /api/v2/users/{userId}
Scope: user:read
def get_user_details(user_id: str) -> dict:
"""
Fetches user details to check current status.
Args:
user_id (str): The UUID of the user.
Returns:
dict: The user object representation.
Raises:
UserManagementApiException: If the user is not found or access is denied.
"""
try:
# The SDK method corresponds to GET /api/v2/users/{userId}
response = user_api.get_user(user_id=user_id)
return response.to_dict()
except UserManagementApiException as e:
if e.status == 404:
print(f"Error: User {user_id} not found.")
raise
elif e.status == 403:
print(f"Error: Forbidden. Check OAuth scopes. Requires 'user:read'.")
raise
else:
print(f"Unexpected error: {e.body}")
raise
# Example usage
USER_ID = "123e4567-e89b-12d3-a456-426614174000" # Replace with actual User ID
user_data = get_user_details(USER_ID)
current_status = user_data.get("status")
print(f"Current Status: {current_status}")
Step 2: The Safe Deactivation Logic
To “delete” the user safely, we must send a PUT request to update the user entity. We must preserve the userId and change the status field to inactive.
Endpoint: PUT /api/v2/users/{userId}
Scope: user:write
Critical Parameter: status.
active: User can log in.inactive: User cannot log in. Historical data remains intact.deleted: This state is generally reserved for internal cleanup after a grace period and should not be set manually via API for standard users.
def deactivate_user(user_id: str, reason: str = "Offboarding via API") -> bool:
"""
Deactivates a user by setting their status to 'inactive'.
This preserves historical interaction data because the User ID remains valid
in the database and linked to past conversations.
Args:
user_id (str): The UUID of the user to deactivate.
reason (str): Optional reason for logging purposes.
Returns:
bool: True if successful, False otherwise.
"""
try:
# 1. Fetch current user data to ensure we don't overwrite other fields
# This is a "Read-Modify-Write" pattern essential for safety.
current_user = user_api.get_user(user_id=user_id)
# 2. Check if already inactive
if current_user.status == "inactive":
print(f"User {user_id} is already inactive. No action taken.")
return True
# 3. Prepare the update payload
# We only send the fields we intend to change, but including the ID is good practice
# Note: In the Python SDK, we modify the object directly
current_user.status = "inactive"
# Optional: Clear sensitive fields if required by policy (e.g., phone number)
# current_user.phone_number = None
# However, keeping the phone number helps in historical reporting analysis.
# 4. Execute the PUT request
# The SDK method update_user corresponds to PUT /api/v2/users/{userId}
updated_user = user_api.update_user(
user_id=user_id,
body=current_user
)
print(f"User {user_id} successfully deactivated. New status: {updated_user.status}")
return True
except UserManagementApiException as e:
print(f"API Error {e.status}: {e.body}")
return False
except Exception as e:
print(f"Unexpected error: {str(e)}")
return False
# Execute deactivation
success = deactivate_user(USER_ID)
Step 3: Handling Associated Resources (Queues, Skills, Routing)
Deactivating a user does not automatically remove them from Queues, Routing Profiles, or Skills Groups. While this does not break historical data, it can cause confusion in current workforce management reporting. It is best practice to remove the user from active queues before or after deactivation.
Endpoint: DELETE /api/v2/users/{userId}/queues/{queueId}
Scope: routing:queue:write
from purecloud_platform_client import RoutingQueuesApi
def remove_user_from_queues(user_id: str, queue_ids: list[str]) -> None:
"""
Removes a user from specified queues.
Args:
user_id (str): The UUID of the user.
queue_ids (list[str]): List of Queue UUIDs to remove the user from.
"""
routing_api = RoutingQueuesApi(configuration=config)
for queue_id in queue_ids:
try:
# DELETE /api/v2/users/{userId}/queues/{queueId}
routing_api.delete_user_queue(
user_id=user_id,
queue_id=queue_id
)
print(f"Removed user {user_id} from queue {queue_id}")
except UserManagementApiException as e:
# 404 might mean the user is not in this queue, which is acceptable
if e.status == 404:
print(f"User {user_id} not found in queue {queue_id}. Skipping.")
else:
print(f"Error removing user from queue {queue_id}: {e.body}")
except Exception as e:
print(f"Unexpected error removing from queue: {str(e)}")
# Example: Remove user from a specific queue before deactivation
# QUEUE_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# remove_user_from_queues(USER_ID, [QUEUE_ID])
Complete Working Example
This script combines authentication, status checking, queue removal, and deactivation into a single runnable module.
import os
import sys
from dotenv import load_dotenv
from purecloud_platform_client import (
Configuration,
PureCloudAuthFlow,
UserManagementApi,
RoutingQueuesApi,
UserManagementApiException
)
def main():
# 1. Load Configuration
load_dotenv()
if not os.getenv("GENESYS_CLIENT_ID") or not os.getenv("GENESYS_CLIENT_SECRET"):
print("Error: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in .env")
sys.exit(1)
config = Configuration()
config.host = os.getenv("GENESYS_DOMAIN", "api.mypurecloud.com")
config.access_token_url = f"https://{os.getenv('GENESYS_DOMAIN', 'api.mypurecloud.com')}/oauth/token"
config.client_id = os.getenv("GENESYS_CLIENT_ID")
config.client_secret = os.getenv("GENESYS_CLIENT_SECRET")
config.auth_flow = PureCloudAuthFlow.CLIENT_CREDENTIALS
user_api = UserManagementApi(configuration=config)
routing_api = RoutingQueuesApi(configuration=config)
USER_ID = os.getenv("TARGET_USER_ID")
if not USER_ID:
print("Error: TARGET_USER_ID must be set in .env")
sys.exit(1)
# 2. Fetch User
try:
user = user_api.get_user(user_id=USER_ID)
print(f"Found User: {user.name} ({user.id})")
print(f"Current Status: {user.status}")
except UserManagementApiException as e:
print(f"Failed to fetch user: {e.status} - {e.body}")
sys.exit(1)
# 3. Check Status
if user.status == "inactive":
print("User is already inactive. Exiting.")
return
# 4. Remove from Queues (Optional but Recommended)
# You would typically fetch the user's queues via GET /api/v2/users/{userId}/queues
# For this example, we assume a known list or skip this step if not needed.
# queues = user_api.get_user_queues(user_id=USER_ID)
# for q in queues.entities:
# try:
# routing_api.delete_user_queue(user_id=USER_ID, queue_id=q.id)
# except Exception as e:
# print(f"Could not remove from queue {q.id}: {e}")
# 5. Deactivate User
try:
user.status = "inactive"
# Note: The update_user method sends a PUT request with the full user object
# It is safe because we fetched it immediately before.
updated_user = user_api.update_user(user_id=USER_ID, body=user)
print(f"Successfully deactivated user. New Status: {updated_user.status}")
except UserManagementApiException as e:
print(f"Failed to deactivate user: {e.status} - {e.body}")
sys.exit(1)
except Exception as e:
print(f"Unexpected error: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 409 Conflict - “User cannot be deleted because they have historical data”
Cause: You attempted to use DELETE /api/v2/users/{userId} directly. Genesys Cloud prevents hard deletion of users who have participated in conversations to maintain the integrity of the analytics database.
Fix: Do not use the Delete endpoint. Use the Update endpoint to set status to inactive as shown in Step 2. The user ID remains in the system, but the account is closed.
Error: 403 Forbidden - “Insufficient permissions”
Cause: The OAuth token associated with the client lacks the user:write scope.
Fix:
- Go to Genesys Cloud Admin Console.
- Navigate to Platform Settings > API Access.
- Find your OAuth Client.
- Edit the Client and ensure
user:writeanduser:readare checked. - Re-generate the token.
Error: 429 Too Many Requests
Cause: You are processing a large batch of users and exceeding the rate limit (typically 10-30 requests per second depending on the endpoint and organization tier).
Fix: Implement exponential backoff.
import time
def api_call_with_retry(func, *args, retries=3, backoff_factor=2):
for attempt in range(retries):
try:
return func(*args)
except UserManagementApiException as e:
if e.status == 429:
wait_time = backoff_factor ** attempt
print(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time)
else:
raise
raise Exception("Max retries exceeded")
# Usage
# api_call_with_retry(user_api.update_user, user_id=USER_ID, body=user)
Error: 400 Bad Request - “Invalid User Status”
Cause: You attempted to set the status to a value that is not allowed, such as deleted directly via API, or a typo like in-active.
Fix: Ensure the status string is exactly "inactive" (lowercase). Valid statuses for standard users are active and inactive.