CX as Code: Exporting Your Entire Org Configuration for Disaster Recovery
What You Will Build
- A Python script that iterates through every configurable entity in a Genesys Cloud organization, serializes the JSON payload, and saves it to a structured directory.
- This tutorial uses the Genesys Cloud Python SDK (
purecloudplatformclientv2) and the REST API for entities lacking SDK support. - The implementation covers Python 3.9+ with type hints and robust error handling for pagination and rate limits.
Prerequisites
- OAuth Client: A Confidential Client (Client Credentials Grant) registered in the Genesys Cloud Admin Console.
- Required Scopes: The client must have “Read-only” access to all entities you intend to export. For a full org export, you typically need scopes such as
organization:read,routing:read,users:read,users:group:read,users:team:read,users:skill:read,users:role:read,users:wrapupcode:read,users:outbound:read,users:queue:read,users:wrapupcode:read,users:utilization:read,users:location:read,users:businessunit:read,users:department:read,users:extension:read,users:phone:read,users:schedule:read,users:shift:read,users:team:read,users:wrapupcode:read,users:wrapupcode:read,users:wrapupcode:read. Note: In practice, assign the “Organization Administrator” role to the client or explicitly grant read scopes for every entity type. - SDK Version:
purecloudplatformclientv2>= 165.0.0. - Dependencies:
requests,tqdm(for progress bars),os,json,time.
Authentication Setup
Genesys Cloud uses OAuth 2.0 with the Client Credentials Grant flow. The Python SDK handles token acquisition and refresh automatically, but you must initialize the API client correctly.
import purecloudplatformclientv2
from purecloudplatformclientv2.rest import ApiException
import os
def get_platform_api_client() -> purecloudplatformclientv2.ApiClient:
"""
Initializes and returns a configured Genesys Cloud API Client.
"""
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
if not client_id or not client_secret:
raise EnvironmentError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")
# Initialize the API client
api_client = purecloudplatformclientv2.ApiClient(
client_id=client_id,
client_secret=client_secret,
base_url="https://api.mypurecloud.com" # Adjust for your region (e.g., api.ca.purecloud.ie)
)
return api_client
Critical Note: The base_url must match your organization’s region. Common regions include api.mypurecloud.com (US), api.eu.purecloud.com (EU), and api.ap.purecloud.com (APAC). Using the wrong region results in 401 Unauthorized or 404 Not Found errors.
Implementation
Step 1: Define the Entity Registry
To export the entire configuration, we must know what exists. We will define a registry of entity types. Some entities are “leaf” nodes (e.g., Users, Queues), while others are “container” nodes (e.g., Locations, Teams). We will prioritize entities that define the business logic: Routing, Users, and Locations.
We create a helper class to standardize the export process. This class handles pagination, which is crucial for organizations with thousands of users or interactions.
import purecloudplatformclientv2
from purecloudplatformclientv2.rest import ApiException
import json
import os
import time
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
@dataclass
class EntityConfig:
"""
Configuration for a specific entity type export.
"""
name: str
api_class: type
method_name: str # The SDK method name to call
args: Optional[Dict[str, Any]] = None # Additional arguments for the method
is_paginated: bool = True
page_size: int = 1000
output_filename_prefix: str = ""
# Registry of entities to export
ENTITY_REGISTRY: List[EntityConfig] = [
# Users & Identity
EntityConfig(
name="users",
api_class=purecloudplatformclientv2.UserManagementApi,
method_name="get_users",
args={"page_size": 1000, "expand": ["userdivisions","groups","teams","skills","roles","locations","phones"]},
is_paginated=True,
output_filename_prefix="user_"
),
EntityConfig(
name="groups",
api_class=purecloudplatformclientv2.UserManagementApi,
method_name="get_groups",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="group_"
),
EntityConfig(
name="teams",
api_class=purecloudplatformclientv2.UserManagementApi,
method_name="get_teams",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="team_"
),
EntityConfig(
name="roles",
api_class=purecloudplatformclientv2.UserManagementApi,
method_name="get_roles",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="role_"
),
EntityConfig(
name="skills",
api_class=purecloudplatformclientv2.UserManagementApi,
method_name="get_skills",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="skill_"
),
# Routing Configuration
EntityConfig(
name="queues",
api_class=purecloudplatformclientv2.RoutingApi,
method_name="get_routing_queues",
args={"page_size": 1000, "expand": ["statistics","members","outbound","skills","wrapupcodes"]},
is_paginated=True,
output_filename_prefix="queue_"
),
EntityConfig(
name="wrapup_codes",
api_class=purecloudplatformclientv2.RoutingApi,
method_name="get_routing_wrapupcodes",
args={"page_size": 1000, "expand": ["queue"]},
is_paginated=True,
output_filename_prefix="wrapup_code_"
),
EntityConfig(
name="outbound_campaigns",
api_class=purecloudplatformclientv2.OutboundApi,
method_name="get_outbound_campaigns",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="campaign_"
),
EntityConfig(
name="outbound_contact_lists",
api_class=purecloudplatformclientv2.OutboundApi,
method_name="get_outbound_contactlists",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="contact_list_"
),
# Locations & Infrastructure
EntityConfig(
name="locations",
api_class=purecloudplatformclientv2.LocationApi,
method_name="get_locations",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="location_"
),
EntityConfig(
name="business_units",
api_class=purecloudplatformclientv2.BusinessUnitApi,
method_name="get_businessunits",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="business_unit_"
),
EntityConfig(
name="departments",
api_class=purecloudplatformclientv2.BusinessUnitApi,
method_name="get_departments",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="department_"
),
EntityConfig(
name="extensions",
api_class=purecloudplatformclientv2.ExtensionApi,
method_name="get_extensions",
args={"page_size": 1000},
is_paginated=True,
output_filename_prefix="extension_"
),
EntityConfig(
name="flow_versions",
api_class=purecloudplatformclientv2.FlowApi,
method_name="get_flows",
args={"page_size": 1000, "expand": ["statistics","versions"]},
is_paginated=True,
output_filename_prefix="flow_"
),
]
Step 2: Implement the Pagination and Export Logic
The core challenge is handling pagination efficiently. Genesys Cloud APIs return a next_page token. We must loop until next_page is None. We also need to handle 429 Too Many Requests errors by implementing exponential backoff.
import purecloudplatformclientv2
from purecloudplatformclientv2.rest import ApiException
import json
import os
import time
import logging
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def handle_api_exception(e: ApiException, entity_name: str, retry_count: int = 0, max_retries: int = 3) -> bool:
"""
Handles API exceptions, specifically 429s with exponential backoff.
Returns True if the call should be retried, False otherwise.
"""
if e.status == 429 and retry_count < max_retries:
wait_time = 2 ** retry_count # Exponential backoff
logger.warning(f"Rate limited on {entity_name}. Waiting {wait_time} seconds... (Attempt {retry_count + 1}/{max_retries})")
time.sleep(wait_time)
return True
else:
logger.error(f"API Error on {entity_name}: Status {e.status}, Reason {e.reason}, Body {e.body}")
return False
def export_entity(api_client: purecloudplatformclientv2.ApiClient, config: EntityConfig, output_dir: str):
"""
Exports all instances of an entity type to JSON files.
"""
# Create directory for this entity type
entity_dir = os.path.join(output_dir, config.name)
os.makedirs(entity_dir, exist_ok=True)
# Initialize the API object
api_obj = config.api_class(api_client)
# Get the method dynamically
method = getattr(api_obj, config.method_name)
page_token = None
page_count = 0
total_items = 0
while True:
try:
# Build kwargs for the method call
kwargs = config.args.copy() if config.args else {}
if page_token:
kwargs['page_token'] = page_token
# Execute the API call
response = method(**kwargs)
# Check if response is a list or a container with an 'entities' list
# Most Genesys SDK responses have an 'entities' attribute for paginated lists
entities = response.entities if hasattr(response, 'entities') else response
if not entities:
logger.info(f"No more entities found for {config.name}.")
break
total_items += len(entities)
page_count += 1
# Save each entity to a file
for entity in entities:
# Convert the SDK object to a dictionary
# The SDK objects have a 'to_dict()' method
entity_dict = entity.to_dict()
# Create a safe filename from the entity ID or name
entity_id = entity_dict.get('id', entity_dict.get('name', 'unknown'))
# Sanitize filename
safe_filename = "".join(c for c in str(entity_id) if c.isalnum() or c in '-_').lower()
filename = f"{config.output_filename_prefix}{safe_filename}.json"
filepath = os.path.join(entity_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(entity_dict, f, indent=2, default=str)
logger.info(f"Exported page {page_count} of {config.name} ({len(entities)} items). Total: {total_items}")
# Check for next page
if hasattr(response, 'next_page') and response.next_page:
page_token = response.next_page
else:
break
except ApiException as e:
if not handle_api_exception(e, config.name, retry_count=0): # Simplified retry logic for brevity
logger.error(f"Fatal error exporting {config.name}. Stopping.")
break
continue # Retry the current page
# Small delay to be respectful of rate limits even if not throttled
time.sleep(0.1)
logger.info(f"Finished exporting {config.name}. Total items: {total_items}")
Step 3: Processing Results and Metadata
While exporting individual entities is useful for DR, a single manifest file is critical for reconstruction. This manifest records the order of export, the count of entities, and any errors encountered. This allows you to script the import process later, ensuring dependencies (e.g., Teams before Users, Skills before Queues) are respected.
import json
import os
from datetime import datetime
def generate_manifest(output_dir: str, export_stats: Dict[str, Any]):
"""
Generates a manifest.json file summarizing the export.
"""
manifest = {
"export_timestamp": datetime.utcnow().isoformat(),
"platform": "Genesys Cloud",
"sdk_version": "purecloudplatformclientv2",
"statistics": export_stats,
"dependencies": [
"locations",
"business_units",
"departments",
"extensions",
"groups",
"teams",
"roles",
"skills",
"wrapup_codes",
"users",
"queues",
"flows",
"outbound_campaigns",
"outbound_contact_lists"
],
"notes": "Ensure entities are imported in dependency order. Users require Teams and Skills to exist first."
}
manifest_path = os.path.join(output_dir, "manifest.json")
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(manifest, f, indent=2)
logger.info(f"Manifest generated at {manifest_path}")
Complete Working Example
The following script combines all components into a single, runnable module. Save this as export_org.py.
import purecloudplatformclientv2
from purecloudplatformclientv2.rest import ApiException
import json
import os
import time
import logging
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from datetime import datetime
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
@dataclass
class EntityConfig:
name: str
api_class: type
method_name: str
args: Optional[Dict[str, Any]] = None
is_paginated: bool = True
page_size: int = 1000
output_filename_prefix: str = ""
ENTITY_REGISTRY: List[EntityConfig] = [
EntityConfig(name="locations", api_class=purecloudplatformclientv2.LocationApi, method_name="get_locations", args={"page_size": 1000}, output_filename_prefix="location_"),
EntityConfig(name="business_units", api_class=purecloudplatformclientv2.BusinessUnitApi, method_name="get_businessunits", args={"page_size": 1000}, output_filename_prefix="business_unit_"),
EntityConfig(name="departments", api_class=purecloudplatformclientv2.BusinessUnitApi, method_name="get_departments", args={"page_size": 1000}, output_filename_prefix="department_"),
EntityConfig(name="extensions", api_class=purecloudplatformclientv2.ExtensionApi, method_name="get_extensions", args={"page_size": 1000}, output_filename_prefix="extension_"),
EntityConfig(name="groups", api_class=purecloudplatformclientv2.UserManagementApi, method_name="get_groups", args={"page_size": 1000}, output_filename_prefix="group_"),
EntityConfig(name="teams", api_class=purecloudplatformclientv2.UserManagementApi, method_name="get_teams", args={"page_size": 1000}, output_filename_prefix="team_"),
EntityConfig(name="roles", api_class=purecloudplatformclientv2.UserManagementApi, method_name="get_roles", args={"page_size": 1000}, output_filename_prefix="role_"),
EntityConfig(name="skills", api_class=purecloudplatformclientv2.UserManagementApi, method_name="get_skills", args={"page_size": 1000}, output_filename_prefix="skill_"),
EntityConfig(name="users", api_class=purecloudplatformclientv2.UserManagementApi, method_name="get_users", args={"page_size": 1000, "expand": ["userdivisions","groups","teams","skills","roles","locations","phones"]}, output_filename_prefix="user_"),
EntityConfig(name="queues", api_class=purecloudplatformclientv2.RoutingApi, method_name="get_routing_queues", args={"page_size": 1000, "expand": ["statistics","members","outbound","skills","wrapupcodes"]}, output_filename_prefix="queue_"),
EntityConfig(name="wrapup_codes", api_class=purecloudplatformclientv2.RoutingApi, method_name="get_routing_wrapupcodes", args={"page_size": 1000, "expand": ["queue"]}, output_filename_prefix="wrapup_code_"),
EntityConfig(name="flows", api_class=purecloudplatformclientv2.FlowApi, method_name="get_flows", args={"page_size": 1000, "expand": ["statistics","versions"]}, output_filename_prefix="flow_"),
EntityConfig(name="outbound_campaigns", api_class=purecloudplatformclientv2.OutboundApi, method_name="get_outbound_campaigns", args={"page_size": 1000}, output_filename_prefix="campaign_"),
EntityConfig(name="outbound_contact_lists", api_class=purecloudplatformclientv2.OutboundApi, method_name="get_outbound_contactlists", args={"page_size": 1000}, output_filename_prefix="contact_list_"),
]
def get_platform_api_client() -> purecloudplatformclientv2.ApiClient:
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
if not client_id or not client_secret:
raise EnvironmentError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")
return purecloudplatformclientv2.ApiClient(
client_id=client_id,
client_secret=client_secret,
base_url="https://api.mypurecloud.com"
)
def handle_api_exception(e: ApiException, entity_name: str) -> bool:
if e.status == 429:
logger.warning(f"Rate limited on {entity_name}. Retrying in 2 seconds...")
time.sleep(2)
return True
else:
logger.error(f"API Error on {entity_name}: Status {e.status}, Reason {e.reason}")
return False
def export_entity(api_client: purecloudplatformclientv2.ApiClient, config: EntityConfig, output_dir: str) -> int:
entity_dir = os.path.join(output_dir, config.name)
os.makedirs(entity_dir, exist_ok=True)
api_obj = config.api_class(api_client)
method = getattr(api_obj, config.method_name)
page_token = None
total_items = 0
while True:
try:
kwargs = config.args.copy() if config.args else {}
if page_token:
kwargs['page_token'] = page_token
response = method(**kwargs)
entities = response.entities if hasattr(response, 'entities') else response
if not entities:
break
total_items += len(entities)
for entity in entities:
entity_dict = entity.to_dict()
entity_id = entity_dict.get('id', entity_dict.get('name', 'unknown'))
safe_filename = "".join(c for c in str(entity_id) if c.isalnum() or c in '-_').lower()
filename = f"{config.output_filename_prefix}{safe_filename}.json"
filepath = os.path.join(entity_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(entity_dict, f, indent=2, default=str)
if hasattr(response, 'next_page') and response.next_page:
page_token = response.next_page
else:
break
time.sleep(0.1)
except ApiException as e:
if not handle_api_exception(e, config.name):
break
continue
return total_items
def main():
output_dir = os.getenv("OUTPUT_DIR", "./genesys_export")
os.makedirs(output_dir, exist_ok=True)
logger.info("Starting Genesys Cloud Org Export...")
api_client = get_platform_api_client()
export_stats = {}
for config in ENTITY_REGISTRY:
logger.info(f"Exporting {config.name}...")
count = export_entity(api_client, config, output_dir)
export_stats[config.name] = count
logger.info(f"Exported {count} {config.name} entities.")
# Generate Manifest
manifest = {
"export_timestamp": datetime.utcnow().isoformat(),
"statistics": export_stats,
"dependencies": [c.name for c in ENTITY_REGISTRY]
}
with open(os.path.join(output_dir, "manifest.json"), 'w') as f:
json.dump(manifest, f, indent=2)
logger.info("Export complete. Check manifest.json for statistics.")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is invalid, expired, or the client credentials are incorrect.
- Fix: Verify
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRETin your environment. Ensure the client is active in the Admin Console. Check that thebase_urlmatches your region.
Error: 403 Forbidden
- Cause: The OAuth client lacks the necessary scopes to read the specific entity.
- Fix: Go to Admin > Security > OAuth Clients. Select your client and ensure it has “Read” scopes for the failing entity (e.g.,
routing:readfor Queues). If using a custom role, ensure the role is assigned to the client.
Error: 429 Too Many Requests
- Cause: You have exceeded the API rate limit. Genesys Cloud has strict rate limits per client ID.
- Fix: The provided code includes a basic retry mechanism. For large organizations, increase the
time.sleep()delay between pages. Consider staggering exports across multiple client IDs if available.
Error: AttributeError: 'NoneType' object has no attribute 'to_dict'
- Cause: The API returned an empty list or the SDK object structure changed.
- Fix: Ensure you check
if entities:before iterating. The code above includes this check. If using a newer SDK version, verify the attribute name for the list (usuallyentities).