Bulk-Create Users from CSV Using the Genesys Cloud Python SDK
What You Will Build
- A Python script that reads user data from a local CSV file and creates corresponding user accounts in Genesys Cloud.
- This implementation uses the official
genesyscloudPython SDK (version 12+). - The tutorial covers Python 3.8+ with standard library modules and the official SDK.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Flow) is recommended for server-side bulk operations.
- Required Scopes:
user:write(Required to create users)user:read(Required to verify user existence or check status if needed)routing:skillgroup:read(Optional, if assigning skill groups)routing:team:read(Optional, if assigning teams)
- SDK Version:
genesyscloud>= 12.0.0 (Latest stable release as of 2024). - Language/Runtime: Python 3.8 or higher.
- External Dependencies:
genesyscloud: The official Genesys Cloud SDK.python-dotenv: For secure environment variable management (optional but recommended).
Install the required packages:
pip install genesyscloud python-dotenv
Authentication Setup
The Genesys Cloud SDK handles OAuth token acquisition and refresh automatically once configured. You must provide your client ID, client secret, and environment (e.g., mypurecloud.com or usw2.pure.cloud).
Create a .env file in your project root to store credentials securely:
GENESYS_CLOUD_CLIENT_ID=your_client_id_here
GENESYS_CLOUD_CLIENT_SECRET=your_client_secret_here
GENESYS_CLOUD_ENVIRONMENT=mypurecloud.com
Initialize the SDK in your script. This configuration object is reused across all API calls.
import os
from dotenv import load_dotenv
from genesyscloud.platform.client import PlatformClient
# Load environment variables
load_dotenv()
def get_platform_client() -> PlatformClient:
"""
Initializes and returns a configured Genesys Cloud PlatformClient.
"""
# Retrieve credentials from environment variables
client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
environment = os.getenv("GENESYS_CLOUD_ENVIRONMENT", "mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET must be set in environment.")
# Configure the OAuth credentials
# The SDK will automatically request a token and handle refreshes
platform_client = PlatformClient(
credentials={
'client_id': client_id,
'client_secret': client_secret,
'environment': environment
}
)
return platform_client
Implementation
Step 1: Define the CSV Structure and Parse Data
Before calling the API, you must structure your data. The Genesys Cloud User creation endpoint (POST /api/v2/users) expects a JSON payload with specific fields. Not all fields are mandatory, but a user generally requires at least a name and an email address.
Create a sample CSV file named users_to_create.csv:
first_name,last_name,email,phone_number,division_id
John,Doe,john.doe@example.com,+12025550101,
Jane,Smith,jane.smith@example.com,+12025550102,
Bob,Jones,bob.jones@example.com,+12025550103,
Note: division_id is optional. If omitted, the user is created in the default division. If you assign users to specific divisions, you must provide the valid UUID of that division.
Now, write the Python logic to parse this CSV. We will use the built-in csv module.
import csv
from typing import List, Dict, Any
def parse_csv_users(csv_filepath: str) -> List[Dict[str, Any]]:
"""
Reads a CSV file and returns a list of dictionaries representing user data.
Args:
csv_filepath: Path to the CSV file.
Returns:
List of dictionaries with keys: first_name, last_name, email, phone_number, division_id
"""
users_data = []
with open(csv_filepath, mode='r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
# Basic validation: Email is required for user creation
if not row.get('email'):
print(f"Skipping row due to missing email: {row}")
continue
# Clean up whitespace
cleaned_row = {k: v.strip() if v else v for k, v in row.items()}
users_data.append(cleaned_row)
return users_data
Step 2: Construct the User Creation Payload
The POST /api/v2/users endpoint requires a User object. In the Python SDK, this is represented by genesyscloud.models.user.User. You must map the CSV data to this model.
Critical fields for a basic user:
email: Must be unique.first_name: Required.last_name: Required.phone_numbers: A list of phone number objects. Each phone number needs atype(e.g., “work”, “mobile”) and adisplay_number.
Here is the function to convert a dictionary from Step 1 into an SDK User object:
from genesyscloud.models.user import User
from genesyscloud.models.phone_number import PhoneNumber
def create_user_object(data: Dict[str, Any]) -> User:
"""
Converts a dictionary of user data into a Genesys Cloud User object.
Args:
data: Dictionary containing first_name, last_name, email, etc.
Returns:
A configured User object ready for API submission.
"""
# Initialize the User object
user = User(
first_name=data['first_name'],
last_name=data['last_name'],
email=data['email']
)
# Optional: Add phone number if provided
if data.get('phone_number'):
phone_number = PhoneNumber(
type='work', # Can be 'work', 'home', 'mobile', etc.
display_number=data['phone_number']
)
user.phone_numbers = [phone_number]
# Optional: Assign to a specific division
# Note: If division_id is empty string, we leave it None so it uses default
if data.get('division_id') and data['division_id'] != '':
user.division_id = data['division_id']
return user
Step 3: Execute Bulk Creation with Error Handling
Creating users one by one via API calls is inefficient and prone to rate limiting. However, the Genesys Cloud SDK does not have a native “bulk create users” endpoint in the same way it does for analytics queries. You must loop through the list and call post_users for each user.
To handle this robustly, you must implement:
- Retry Logic: For transient network errors (5xx) or rate limits (429).
- Error Tracking: Record which users succeeded and which failed, along with the error reason.
- Duplicate Handling: If a user with the same email already exists, the API returns a 409 Conflict. We should catch this and log it as a skip rather than a critical failure.
import time
import logging
from genesyscloud.rest import ApiException
from typing import Tuple, List
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def create_single_user(platform_client: PlatformClient, user_obj: User) -> Tuple[bool, str]:
"""
Attempts to create a single user.
Returns:
Tuple: (success: bool, message: str)
"""
try:
# Call the API
# Scope required: user:write
response = platform_client.users.post_users(body=user_obj)
# If successful, response is a User object
logger.info(f"Successfully created user: {response.email} (ID: {response.id})")
return True, f"Created (ID: {response.id})"
except ApiException as e:
# Handle specific HTTP status codes
if e.status == 409:
# Conflict: User with this email likely already exists
logger.warning(f"User already exists or conflict: {user_obj.email}. Skipping.")
return False, "Duplicate/Conflict"
elif e.status == 429:
# Rate Limit: Should ideally be handled by a retry wrapper, but logged here for visibility
logger.error(f"Rate limited while creating user: {user_obj.email}")
return False, "Rate Limited"
elif e.status == 400:
# Bad Request: Invalid data format
logger.error(f"Bad Request for user {user_obj.email}: {e.body}")
return False, f"Bad Request: {e.body}"
else:
logger.error(f"Unexpected error creating user {user_obj.email}: {e.status} - {e.reason}")
return False, f"API Error: {e.status}"
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
return False, f"Unexpected Error: {str(e)}"
def bulk_create_users(platform_client: PlatformClient, users_data: List[Dict[str, Any]], max_retries: int = 3) -> Dict[str, List[str]]:
"""
Iterates through a list of user data dictionaries and creates them in Genesys Cloud.
Args:
platform_client: The authenticated SDK client.
users_data: List of user data dictionaries from CSV.
max_retries: Number of times to retry on 429/5xx errors.
Returns:
Dictionary with keys 'created', 'failed', 'skipped' containing lists of emails.
"""
results = {
'created': [],
'failed': [],
'skipped': []
}
for i, data in enumerate(users_data):
email = data.get('email', 'Unknown')
logger.info(f"Processing user {i+1}/{len(users_data)}: {email}")
success = False
last_error = ""
for attempt in range(max_retries):
try:
user_obj = create_user_object(data)
success, message = create_single_user(platform_client, user_obj)
if success:
results['created'].append(email)
break
elif "Duplicate" in message or "Conflict" in message:
results['skipped'].append(email)
break
elif "Rate Limited" in message or "500" in message:
# Wait before retrying (Exponential backoff)
wait_time = 2 ** attempt
logger.warning(f"Retrying in {wait_time} seconds... (Attempt {attempt+1}/{max_retries})")
time.sleep(wait_time)
continue
else:
# Non-retryable error (e.g., 400 Bad Request)
results['failed'].append(f"{email}: {message}")
break
except Exception as e:
logger.error(f"Unexpected error during processing of {email}: {e}")
results['failed'].append(f"{email}: Unexpected Error")
break
if not success and email not in results['skipped']:
# If loop finished without success/skip, and not already added to failed
if email not in [f.split(':')[0] for f in results['failed']]:
results['failed'].append(f"{email}: Max retries exceeded")
return results
Complete Working Example
This is the full, copy-pasteable script. Save this as bulk_create_users.py.
import os
import csv
import time
import logging
from typing import List, Dict, Any, Tuple
from dotenv import load_dotenv
from genesyscloud.platform.client import PlatformClient
from genesyscloud.models.user import User
from genesyscloud.models.phone_number import PhoneNumber
from genesyscloud.rest import ApiException
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def get_platform_client() -> PlatformClient:
"""Initializes the Genesys Cloud SDK client."""
load_dotenv()
client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
environment = os.getenv("GENESYS_CLOUD_ENVIRONMENT", "mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET must be set.")
return PlatformClient(
credentials={
'client_id': client_id,
'client_secret': client_secret,
'environment': environment
}
)
def parse_csv_users(csv_filepath: str) -> List[Dict[str, Any]]:
"""Parses CSV file into list of user data dictionaries."""
users_data = []
with open(csv_filepath, mode='r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
if not row.get('email'):
logger.warning(f"Skipping row due to missing email: {row}")
continue
# Clean whitespace
cleaned_row = {k: v.strip() if v else v for k, v in row.items()}
users_data.append(cleaned_row)
return users_data
def create_user_object(data: Dict[str, Any]) -> User:
"""Maps CSV data to Genesys Cloud User model."""
user = User(
first_name=data['first_name'],
last_name=data['last_name'],
email=data['email']
)
if data.get('phone_number'):
phone_number = PhoneNumber(
type='work',
display_number=data['phone_number']
)
user.phone_numbers = [phone_number]
if data.get('division_id') and data['division_id'] != '':
user.division_id = data['division_id']
return user
def create_single_user(platform_client: PlatformClient, user_obj: User) -> Tuple[bool, str]:
"""Attempts to create a single user via API."""
try:
response = platform_client.users.post_users(body=user_obj)
logger.info(f"Created user: {response.email} (ID: {response.id})")
return True, f"Created (ID: {response.id})"
except ApiException as e:
if e.status == 409:
logger.warning(f"Conflict (User exists): {user_obj.email}")
return False, "Duplicate"
elif e.status == 429:
logger.warning(f"Rate Limited: {user_obj.email}")
return False, "Rate Limited"
elif e.status == 400:
logger.error(f"Bad Request: {user_obj.email} - {e.body}")
return False, f"Bad Request: {e.body}"
else:
logger.error(f"API Error {e.status}: {user_obj.email}")
return False, f"API Error: {e.status}"
except Exception as e:
logger.error(f"Unexpected error: {e}")
return False, f"Unexpected Error: {str(e)}"
def bulk_create_users(platform_client: PlatformClient, users_data: List[Dict[str, Any]], max_retries: int = 3) -> Dict[str, List[str]]:
"""Executes the bulk creation loop with retry logic."""
results = {'created': [], 'failed': [], 'skipped': []}
for i, data in enumerate(users_data):
email = data.get('email', 'Unknown')
logger.info(f"[{i+1}/{len(users_data)}] Processing: {email}")
success = False
for attempt in range(max_retries):
try:
user_obj = create_user_object(data)
success, message = create_single_user(platform_client, user_obj)
if success:
results['created'].append(email)
break
elif "Duplicate" in message:
results['skipped'].append(email)
break
elif "Rate Limited" in message:
wait_time = 2 ** attempt
logger.info(f"Rate limited. Waiting {wait_time}s before retry...")
time.sleep(wait_time)
continue
else:
results['failed'].append(f"{email}: {message}")
break
except Exception as e:
logger.error(f"Error processing {email}: {e}")
results['failed'].append(f"{email}: System Error")
break
if not success and email not in results['skipped']:
if not any(email in f for f in results['failed']):
results['failed'].append(f"{email}: Max retries exceeded")
return results
def main():
# Configuration
CSV_FILE = 'users_to_create.csv'
# 1. Initialize Client
try:
client = get_platform_client()
except Exception as e:
logger.error(f"Failed to initialize client: {e}")
return
# 2. Parse CSV
try:
users_data = parse_csv_users(CSV_FILE)
logger.info(f"Parsed {len(users_data)} users from CSV.")
except FileNotFoundError:
logger.error(f"CSV file '{CSV_FILE}' not found.")
return
except Exception as e:
logger.error(f"Error parsing CSV: {e}")
return
if not users_data:
logger.warning("No users to process.")
return
# 3. Bulk Create
logger.info("Starting bulk user creation...")
results = bulk_create_users(client, users_data)
# 4. Report Results
logger.info("="*30)
logger.info("SUMMARY")
logger.info("="*30)
logger.info(f"Created: {len(results['created'])}")
logger.info(f"Skipped (Duplicates): {len(results['skipped'])}")
logger.info(f"Failed: {len(results['failed'])}")
if results['failed']:
logger.warning("Failed Users:")
for fail in results['failed']:
logger.warning(f" - {fail}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 409 Conflict
- Cause: A user with the specified email address already exists in the Genesys Cloud organization. Email addresses must be unique per organization.
- Fix: The script above catches this and adds the user to the
skippedlist. If you want to update existing users, you must first search for the user by email usingGET /api/v2/users?email={email}and then usePUT /api/v2/users/{id}instead ofPOST.
Error: 400 Bad Request
- Cause: The payload does not meet validation rules. Common issues include:
- Missing
first_nameorlast_name. - Invalid email format.
- Invalid phone number format (must include country code, e.g.,
+1...). - Invalid
division_id(UUID format incorrect or division does not exist).
- Missing
- Fix: Check the
e.bodyin theApiExceptioncatch block. It usually contains a detailed JSON error message from Genesys Cloud indicating exactly which field failed validation.
Error: 429 Too Many Requests
- Cause: You are hitting the API rate limit. Genesys Cloud enforces limits on the number of requests per minute per client ID.
- Fix: The script implements exponential backoff. If you are creating thousands of users, consider increasing the
max_retriesor adding a small fixed delay (e.g.,time.sleep(1)) between each user creation to stay well under the limit.
Error: 401 Unauthorized
- Cause: The OAuth token is invalid or expired.
- Fix: Ensure your
CLIENT_IDandCLIENT_SECRETare correct. The SDK handles refresh, but if the client credentials are revoked or incorrect, initialization will fail. Check your.envfile.