How to deactivate a user via the API while preserving historical interaction data
What You Will Build
- This tutorial demonstrates how to programmatically deactivate a Genesys Cloud user to remove their access while ensuring all historical conversation data, analytics, and audit logs remain fully intact and attributable.
- The solution uses the Genesys Cloud CX API v2 endpoints for User Management and User Authentication.
- The implementation is provided in Python using the
genesyscloudSDK and rawrequestsfor clarity on HTTP methods.
Prerequisites
- OAuth Client Type: Machine-to-Machine (M2M) or User-to-User (U2U) client with sufficient permissions.
- Required Scopes:
user:read(to retrieve user details)user:write(to update the user status)user:delete(optional, only if performing a hard delete, which is generally discouraged for data preservation)
- SDK Version:
genesys-cloud-python>= 2.0.0 - Language/Runtime: Python 3.9+
- External Dependencies:
pip install genesys-cloud-python requests
Authentication Setup
Genesys Cloud uses OAuth 2.0 for authentication. For API-driven automation, a Machine-to-Machine (M2M) client is the standard approach. You must generate a client ID and client secret in the Genesys Cloud Admin Console under Developers > API Access.
The following code snippet establishes a secure session using the Genesys Cloud Python SDK. It handles the initial token acquisition and automatically manages token refreshes during the lifecycle of the script.
import os
from purecloudplatformclientv2 import (
Configuration,
ApiClient,
UserApi,
AuthenticationApi
)
def get_config():
"""
Configures the Genesys Cloud SDK with environment variables.
Ensure GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET are set.
"""
config = Configuration()
config.host = "https://api.mypurecloud.com" # Adjust for your environment (e.g., api.genesiscloud.com)
config.client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
config.client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
return config
def get_user_api_client():
"""
Returns an initialized UserApi client.
"""
config = get_config()
client = ApiClient(configuration=config)
return UserApi(client)
Implementation
Step 1: Retrieve the User by Email or ID
Before modifying a user, you must identify them uniquely. While you may know the user ID, it is often safer to search by email address to avoid errors if the user ID changes or if you are working with a list of emails from a CSV file.
The UserApi provides the post_users_search endpoint. This endpoint searches for users based on a query string.
def find_user_by_email(api_client: UserApi, email: str) -> dict:
"""
Searches for a user by their email address.
Args:
api_client: The initialized UserApi client.
email: The email address of the user to find.
Returns:
A dictionary containing the user details if found, None otherwise.
"""
try:
# The query parameter supports searching by email, name, and other fields.
# We use a specific query to narrow down results.
response = api_client.post_users_search(
body=f"email:{email}"
)
# The response is a UserEntityListing.
if response.entities and len(response.entities) > 0:
# Return the first matching user.
# Note: If multiple users share an email (rare but possible in some configurations),
# additional logic is needed to distinguish them.
return response.entities[0].to_dict()
else:
print(f"User with email {email} not found.")
return None
except Exception as e:
print(f"Error searching for user: {e}")
return None
Expected Response Structure (simplified):
{
"id": "12345678-1234-1234-1234-123456789abc",
"name": "John Doe",
"email": "john.doe@example.com",
"division": {
"id": "default",
"name": "Default"
},
"status": "ACTIVE",
"addresses": [],
"roles": [],
"skills": []
}
Step 2: Deactivate the User (The Safe “Delete”)
The core requirement is to remove the user’s ability to log in and perform actions without destroying their historical data. In Genesys Cloud, deleting a user (DELETE /api/v2/users/{userId}) is a hard delete. While Genesys Cloud retains conversation data associated with a deleted user ID in the analytics database, the user record itself is gone. This can cause referential integrity issues in downstream reporting tools that join user tables with conversation tables.
The industry-standard best practice is to deactivate the user. This changes the user’s status from ACTIVE to INACTIVE (or DISABLED depending on context). The user record remains in the system, preserving the UUID. All historical interactions (calls, chats, emails) remain linked to this UUID. The user simply cannot authenticate.
We use the PATCH method on /api/v2/users/{userId} to update the status.
def deactivate_user(api_client: UserApi, user_id: str, division_id: str) -> bool:
"""
Deactivates a user by setting their status to INACTIVE.
Args:
api_client: The initialized UserApi client.
user_id: The UUID of the user to deactivate.
division_id: The division ID of the user (required for the patch body).
Returns:
True if successful, False otherwise.
"""
try:
# Construct the patch body.
# Only the fields to be updated need to be included.
patch_body = {
"status": "INACTIVE"
}
# The PATCH request requires the user ID and division ID in the path/headers.
# The SDK handles the division_id parameter automatically if provided.
api_client.patch_user(
user_id=user_id,
division_id=division_id,
body=patch_body
)
print(f"User {user_id} successfully deactivated.")
return True
except Exception as e:
# Handle specific HTTP errors
error_msg = str(e)
if "404" in error_msg:
print(f"User {user_id} not found. It may have already been deleted.")
elif "403" in error_msg:
print(f"Insufficient permissions to deactivate user {user_id}.")
elif "400" in error_msg:
print(f"Bad request. Check the division ID or payload format.")
else:
print(f"Unexpected error deactivating user: {e}")
return False
Why this preserves data:
- Conversation History: Genesys Cloud stores conversations with the
agentIdorparticipantIdas a UUID. Since the user record is not deleted, this UUID remains valid. - Analytics: Reports that filter by “Agent Name” or “Agent ID” will continue to return data for this user. If the user were deleted, some UI filters might fail to resolve the name, though the data would still exist in the raw tables. Deactivation keeps the name and ID resolvable.
- Compliance: Audit logs show who performed actions. If a user is deleted, the audit log shows a UUID. If deactivated, it shows the name and UUID, which is more human-readable for compliance reviews.
Step 3: Verify Deactivation and Check for Active Sessions
After deactivation, it is good practice to verify the status change. Additionally, if the user is currently logged in, they may retain an active session until their token expires (default 1 hour). To force immediate logout, you can revoke their tokens.
def verify_user_status(api_client: UserApi, user_id: str) -> str:
"""
Retrieves the current status of a user.
Args:
api_client: The initialized UserApi client.
user_id: The UUID of the user.
Returns:
The current status string (e.g., 'ACTIVE', 'INACTIVE').
"""
try:
user = api_client.get_user(user_id=user_id)
return user.status
except Exception as e:
print(f"Error verifying user status: {e}")
return None
def revoke_user_tokens(auth_api_client: AuthenticationApi, user_id: str) -> bool:
"""
Revokes all active tokens for a user, forcing immediate logout.
This uses the Authentication API.
Args:
auth_api_client: The initialized AuthenticationApi client.
user_id: The UUID of the user.
Returns:
True if successful.
"""
try:
# The revoke endpoint is POST /api/v2/authentication/revoke
# It accepts a body with the user ID to revoke all tokens for that user.
auth_api_client.post_authentication_revoke(
body={
"userId": user_id
}
)
print(f"All tokens revoked for user {user_id}.")
return True
except Exception as e:
print(f"Error revoking tokens: {e}")
return False
Complete Working Example
The following script combines all steps into a single executable module. It searches for a user by email, deactivates them, verifies the status, and revokes their current sessions.
import os
import sys
from purecloudplatformclientv2 import (
Configuration,
ApiClient,
UserApi,
AuthenticationApi
)
def main():
# 1. Setup Configuration
config = Configuration()
config.host = "https://api.mypurecloud.com"
config.client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
config.client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
if not config.client_id or not config.client_secret:
print("Error: Missing GENESYS_CLOUD_CLIENT_ID or GENESYS_CLOUD_CLIENT_SECRET environment variables.")
sys.exit(1)
client = ApiClient(configuration=config)
user_api = UserApi(client)
auth_api = AuthenticationApi(client)
# 2. Define Target User
target_email = "john.doe@example.com" # Replace with actual email
print(f"Searching for user: {target_email}")
# 3. Find User
user_data = find_user_by_email(user_api, target_email)
if not user_data:
print("User not found. Exiting.")
return
user_id = user_data['id']
division_id = user_data['division']['id']
current_status = user_data['status']
print(f"Found User: {user_data['name']} (ID: {user_id})")
print(f"Current Status: {current_status}")
# 4. Check if already inactive
if current_status == "INACTIVE":
print("User is already inactive. No action needed.")
return
# 5. Deactivate User
print("Deactivating user...")
success = deactivate_user(user_api, user_id, division_id)
if not success:
print("Failed to deactivate user. Exiting.")
return
# 6. Verify Status
print("Verifying status...")
new_status = verify_user_status(user_api, user_id)
if new_status == "INACTIVE":
print("Verification successful: User is now INACTIVE.")
else:
print(f"Verification failed: User status is {new_status}.")
# 7. Revoke Tokens (Force Logout)
print("Revoking active sessions...")
revoke_user_tokens(auth_api, user_id)
print("Process complete.")
# Helper functions from previous steps included here for completeness
def find_user_by_email(api_client: UserApi, email: str) -> dict:
try:
response = api_client.post_users_search(body=f"email:{email}")
if response.entities and len(response.entities) > 0:
return response.entities[0].to_dict()
return None
except Exception as e:
print(f"Error searching for user: {e}")
return None
def deactivate_user(api_client: UserApi, user_id: str, division_id: str) -> bool:
try:
patch_body = {"status": "INACTIVE"}
api_client.patch_user(user_id=user_id, division_id=division_id, body=patch_body)
return True
except Exception as e:
print(f"Error deactivating user: {e}")
return False
def verify_user_status(api_client: UserApi, user_id: str) -> str:
try:
user = api_client.get_user(user_id=user_id)
return user.status
except Exception as e:
print(f"Error verifying status: {e}")
return None
def revoke_user_tokens(auth_api_client: AuthenticationApi, user_id: str) -> bool:
try:
auth_api_client.post_authentication_revoke(body={"userId": user_id})
return True
except Exception as e:
print(f"Error revoking tokens: {e}")
return False
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
What causes it: The OAuth token used does not have the user:write scope.
How to fix it:
- Go to Genesys Cloud Admin Console > Developers > API Access.
- Select your client.
- Ensure the
user:writescope is checked. - Regenerate the client secret if you are using an existing token, as scope changes require a new token.
Error: 400 Bad Request - “Division ID mismatch”
What causes it: The division_id provided in the PATCH request does not match the user’s actual division.
How to fix it:
Always retrieve the user’s division ID from the GET or POST ... search response before attempting the PATCH. Do not hardcode default unless you are certain the user is in the default division.
# Correct way to extract division ID
division_id = user_data['division']['id']
Error: 429 Too Many Requests
What causes it: You are calling the API too frequently. Genesys Cloud has rate limits per client and per tenant.
How to fix it: Implement exponential backoff.
import time
def safe_api_call(func, *args, **kwargs):
"""
Wrapper to handle 429 errors with exponential backoff.
"""
max_retries = 3
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if "429" in str(e):
wait_time = 2 ** attempt
print(f"Rate limit hit. Waiting {wait_time} seconds...")
time.sleep(wait_time)
else:
raise e
raise Exception("Max retries exceeded for 429 error.")
Error: User Still Appears Active in UI
What causes it: Browser caching or an active session that has not been revoked.
How to fix it:
- Ensure
revoke_user_tokenswas called. - Ask the user to clear their browser cache.
- Note that some internal caches in Genesys Cloud may take a few minutes to propagate, but the API response will reflect the change immediately.