How to Bulk-Create Genesys Cloud Users from CSV Using the Python SDK

How to Bulk-Create Genesys Cloud Users from CSV Using the Python SDK

What You Will Build

You will build a Python script that reads a CSV file containing user details (name, email, phone number) and creates corresponding users in Genesys Cloud using the official Platform SDK. The script handles authentication, pagination-free bulk operations, and error logging for failed creations. This tutorial covers the genesyscloud Python SDK.

Prerequisites

  • OAuth Client: You need a Genesys Cloud OAuth client with the user:write scope. For testing, a Public Client or Confidential Client works; for production, use a Confidential Client with Client Credentials flow.
  • SDK Version: genesyscloud Python SDK version 165.0.0 or later.
  • Runtime: Python 3.9 or higher.
  • Dependencies:
    • genesyscloud (Install via pip install genesyscloud)
    • pandas (Optional, but recommended for robust CSV parsing. Install via pip install pandas)

Authentication Setup

The Genesys Cloud Python SDK handles OAuth token acquisition and refresh automatically if you provide the correct credentials. You do not need to manually manage token expiration.

You must configure the SDK with your Organization ID, Client ID, and Client Secret.

import os
from genesyscloud.platform_client import PlatformClient
from genesyscloud.auth import AuthClient

# Configuration
ORGANIZATION_ID = os.getenv("GENESYS_ORG_ID")
CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")

# Initialize the Platform Client
platform_client = PlatformClient(
    org_id=ORGANIZATION_ID,
    client_id=CLIENT_ID,
    client_secret=CLIENT_SECRET
)

# Verify authentication by fetching the current user info
try:
    auth_client = platform_client.auth
    user_info = auth_client.get_user_me()
    print(f"Authenticated as: {user_info.name} ({user_info.email})")
except Exception as e:
    print(f"Authentication failed: {e}")
    exit(1)

Required Scope: user:write

If your OAuth client does not have this scope, the API will return a 403 Forbidden error during the creation step. Ensure your client in the Admin Console has the Users permission set with Create enabled.

Implementation

Step 1: Prepare the CSV Data

The script expects a CSV file with specific columns. Genesys Cloud requires a unique email address for every user. Duplicate emails will cause the API to reject the request.

Create a file named users.csv:

firstName,lastName,email,phoneNumber,divisionId
John,Doe,john.doe@example.com,+1-202-555-0147,00000000-0000-0000-0000-000000000000
Jane,Smith,jane.smith@example.com,+1-202-555-0148,00000000-0000-0000-0000-000000000000

Note on Division: The divisionId is critical. If omitted, the user is created in the default division. It is best practice to specify it. The ID 00000000-0000-0000-0000-000000000000 represents the default division. To find your specific division ID, use the GET /api/v2/organizations/divisions endpoint.

Step 2: Parse CSV and Construct User Objects

We will use pandas to read the CSV and map columns to the genesyscloud.users.models.User object.

import pandas as pd
from genesyscloud.users.models import User, PhoneNumber

def parse_csv_to_users(csv_path: str, division_id: str = "00000000-0000-0000-0000-000000000000") -> list[User]:
    """
    Reads a CSV file and returns a list of User objects ready for API submission.
    """
    # Read CSV
    df = pd.read_csv(csv_path)
    
    users_to_create = []
    
    for index, row in df.iterrows():
        # Construct the primary phone number object
        phone_number = PhoneNumber(
            e164=str(row['phoneNumber']),
            type="work"
        )
        
        # Construct the User object
        user = User(
            first_name=str(row['firstName']),
            last_name=str(row['lastName']),
            email=str(row['email']),
            division_id=division_id,
            phone_numbers=[phone_number]
        )
        
        users_to_create.append(user)
        
    return users_to_create

Step 3: Execute Bulk Creation with Error Handling

The Genesys Cloud API provides a specific endpoint for creating users in bulk: POST /api/v2/users. This endpoint accepts an array of user objects. It is more efficient than calling POST /api/v2/users in a loop because it reduces HTTP overhead and allows the server to validate constraints atomically where possible.

However, the Python SDK does not have a single method post_users_bulk. You must use the UsersApi class and the post_users method, which supports a list of users.

from genesyscloud.users import UsersApi
import logging

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def create_users_bulk(platform_client: PlatformClient, users: list[User]) -> dict:
    """
    Sends a bulk create request to Genesys Cloud.
    """
    users_api = UsersApi(platform_client)
    
    try:
        # The post_users method in the SDK accepts a list of User objects
        # It maps to POST /api/v2/users
        response = users_api.post_users(
            body=users,
            expand=["groups", "teams", "skills", "queue_skills", "routing_skills"]
        )
        
        logger.info(f"Successfully created {len(response.entities)} users.")
        return response
        
    except Exception as e:
        logger.error(f"Bulk creation failed: {e}")
        raise e

Important: The expand parameter is optional but recommended. It ensures that if you assign users to groups or teams during creation, those relationships are returned in the response. For simple creation, you can omit it.

Step 4: Handle Partial Failures and Validation

The bulk endpoint returns a response containing entities (successful creations) and errors (failed creations). You must check the errors array to log which users failed and why.

def process_response(response) -> None:
    """
    Logs successful creations and errors from the bulk response.
    """
    if response.entities:
        for entity in response.entities:
            logger.info(f"Created User: {entity.email} (ID: {entity.id})")
            
    if response.errors:
        for error in response.errors:
            logger.error(f"Failed to create user: {error.message} (Code: {error.code})")
            if hasattr(error, 'entity'):
                logger.error(f"  Problematic User Data: {error.entity}")

Complete Working Example

This is the full, runnable script. Save it as create_users.py.

import os
import pandas as pd
import logging
from genesyscloud.platform_client import PlatformClient
from genesyscloud.users import UsersApi
from genesyscloud.users.models import User, PhoneNumber

# Configure Logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def main():
    # 1. Configuration
    org_id = os.getenv("GENESYS_ORG_ID")
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")
    csv_path = "users.csv"
    division_id = "00000000-0000-0000-0000-000000000000" # Default division

    if not all([org_id, client_id, client_secret]):
        logger.error("Missing environment variables: GENESYS_ORG_ID, GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET")
        return

    # 2. Initialize SDK
    try:
        platform_client = PlatformClient(
            org_id=org_id,
            client_id=client_id,
            client_secret=client_secret
        )
        logger.info("Platform client initialized.")
    except Exception as e:
        logger.error(f"Failed to initialize client: {e}")
        return

    # 3. Parse CSV
    try:
        df = pd.read_csv(csv_path)
        logger.info(f"Read {len(df)} rows from {csv_path}")
        
        users_to_create = []
        for index, row in df.iterrows():
            # Validate required fields
            if pd.isna(row['email']) or pd.isna(row['firstName']) or pd.isna(row['lastName']):
                logger.warning(f"Skipping row {index}: Missing required fields.")
                continue
            
            # Construct Phone Number
            phone_num = None
            if not pd.isna(row['phoneNumber']):
                phone_num = PhoneNumber(
                    e164=str(row['phoneNumber']),
                    type="work"
                )
            
            # Construct User
            user = User(
                first_name=str(row['firstName']),
                last_name=str(row['lastName']),
                email=str(row['email']),
                division_id=division_id,
                phone_numbers=[phone_num] if phone_num else None
            )
            users_to_create.append(user)

    except FileNotFoundError:
        logger.error(f"CSV file not found: {csv_path}")
        return
    except Exception as e:
        logger.error(f"Error parsing CSV: {e}")
        return

    if not users_to_create:
        logger.info("No valid users to create.")
        return

    # 4. Bulk Create
    users_api = UsersApi(platform_client)
    
    try:
        # The SDK's post_users method handles the bulk endpoint
        response = users_api.post_users(
            body=users_to_create,
            expand=[] # No expansion needed for basic creation
        )
        
        logger.info(f"API Call Complete. Status Code: {response.status_code}")
        
        # 5. Process Results
        if response.entities:
            logger.info(f"Successfully created {len(response.entities)} users.")
            for u in response.entities:
                logger.info(f"  - {u.email} (ID: {u.id})")
                
        if response.errors:
            logger.warning(f"Encountered {len(response.errors)} errors.")
            for err in response.errors:
                logger.error(f"Error: {err.message} | Code: {err.code}")
                
    except Exception as e:
        logger.error(f"Bulk creation failed: {e}")
        # Check for 429 Rate Limiting
        if hasattr(e, 'status_code') and e.status_code == 429:
            logger.error("Rate limited. Wait before retrying.")
        elif hasattr(e, 'status_code') and e.status_code == 400:
            logger.error("Bad Request. Check CSV format and user constraints.")

if __name__ == "__main__":
    main()

How to Run

  1. Set your environment variables:
    export GENESYS_ORG_ID="your-org-id"
    export GENESYS_CLIENT_ID="your-client-id"
    export GENESYS_CLIENT_SECRET="your-client-secret"
    
  2. Ensure users.csv is in the same directory.
  3. Run the script:
    python create_users.py
    

Common Errors & Debugging

Error: 400 Bad Request - Duplicate Email

Cause: The CSV contains an email address that already exists in your Genesys Cloud organization. User emails must be unique.
Fix: Clean your CSV data before submission. You can query existing users via GET /api/v2/users?email={email} to check for duplicates, or simply handle the error in the response loop.

Error: 403 Forbidden - Insufficient Permissions

Cause: The OAuth client used does not have the user:write scope.
Fix: Go to the Genesys Cloud Admin Console > Security > OAuth > Clients. Edit your client and add the user:write scope. If using a Confidential Client, ensure the role assigned to the client has Create permission for Users.

Error: 429 Too Many Requests

Cause: You are sending bulk requests too frequently. Genesys Cloud enforces rate limits per tenant and per client.
Fix: Implement exponential backoff. The Python SDK does not auto-retry 429s by default in all methods, so you should wrap the API call in a retry loop.

import time

def retry_on_429(func, *args, max_retries=3, **kwargs):
    for attempt in range(max_retries):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            if hasattr(e, 'status_code') and e.status_code == 429:
                wait_time = 2 ** attempt  # Exponential backoff
                logger.warning(f"Rate limited. Retrying in {wait_time} seconds...")
                time.sleep(wait_time)
            else:
                raise e
    raise Exception("Max retries exceeded")

# Usage
# response = retry_on_429(users_api.post_users, body=users_to_create)

Error: 400 Bad Request - Invalid Phone Number

Cause: The phone number in the CSV is not in E.164 format (e.g., +12025550147 is valid, (202) 555-0147 is not).
Fix: Pre-process the CSV to ensure all phone numbers are in E.164 format. You can use the phonenumbers library in Python to validate and format numbers.

import phonenumbers

def format_e164(phone_str: str) -> str:
    try:
        parsed = phonenumbers.parse(phone_str, "US") # Default region
        return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
    except phonenumbers.NumberParseException:
        raise ValueError(f"Invalid phone number: {phone_str}")

Official References