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 lateststatusfield behaviors). - Language/Runtime: Python 3.9 or higher.
- External Dependencies:
genesyscloudpython-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:
- Prevents the user from logging in.
- Removes them from active routing queues (depending on specific queue settings, but generally they stop receiving new work).
- Preserves all past interactions (calls, chats, messages) associated with their UUID.
- 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:writescope. - 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:writepermission 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/queryendpoint again without thestatus: activefilter to see if the user exists in an inactive state.