Updating Genesys Cloud Routing Queue Wrap-Up Codes via REST API with Python SDK
What You Will Build
- A Python module that fetches, validates, and atomically updates routing queue wrap-up codes using the Genesys Cloud Python SDK.
- The module constructs update payloads with queue ID references, wrap-up code type matrices, and duration limit directives.
- The implementation includes schema validation against routing engine constraints, atomic PUT execution, latency tracking, audit logging, and external WFM webhook synchronization.
Prerequisites
- OAuth confidential client with
routing:queue:readandrouting:queue:writescopes - Genesys Cloud Python SDK (
genesys-cloud-pythonversion 130.0.0 or higher) - Python 3.9 runtime
- External dependencies:
requests,pydantic(for payload validation),logging - Active queue ID with existing wrap-up code configuration
Authentication Setup
The Genesys Cloud Python SDK handles OAuth 2.0 client credentials flow automatically. You must configure the client ID and secret, then initialize the platform client. The SDK caches the access token and handles refresh cycles transparently.
import os
from platformclientv2 import PureCloudPlatformClientV2, QueueApi
from platformclientv2.rest import ApiException
def initialize_genesys_client() -> PureCloudPlatformClientV2:
client = PureCloudPlatformClientV2()
client_id = os.environ.get("GENESYS_CLIENT_ID")
client_secret = os.environ.get("GENESYS_CLIENT_SECRET")
try:
client.login_client_credential(client_id, client_secret)
except ApiException as e:
if e.status == 401:
raise RuntimeError("OAuth 401: Invalid client credentials or expired secret.")
elif e.status == 403:
raise RuntimeError("OAuth 403: Client lacks required routing:queue scopes.")
else:
raise RuntimeError(f"OAuth authentication failed with status {e.status}: {e.body}")
return client
The authentication cycle translates to the following HTTP request:
- Method:
POST - Path:
/oauth/token - Headers:
Content-Type: application/x-www-form-urlencoded,Authorization: Basic <base64(client_id:client_secret)> - Body:
grant_type=client_credentials&scope=routing:queue:read%20routing:queue:write - Response:
{"access_token": "eyJ...", "token_type": "bearer", "expires_in": 1800}
Implementation
Step 1: Fetch Current Wrap-Up Codes and Prepare Context
You must retrieve the existing wrap-up code configuration before modification. The SDK provides a direct method for this endpoint. The response contains the current list of codes, which serves as the baseline for diff calculation and conflict detection.
def fetch_existing_codes(queue_api: QueueApi, queue_id: str) -> list:
try:
response = queue_api.get_queue_wrapup_codes(queue_id)
return response.wrapup_codes if response.wrapup_codes else []
except ApiException as e:
if e.status == 404:
raise RuntimeError(f"Queue {queue_id} does not exist.")
elif e.status == 429:
raise RuntimeError("Rate limit exceeded. Implement exponential backoff.")
else:
raise RuntimeError(f"Fetch failed with status {e.status}: {e.body}")
HTTP equivalent for fetch:
- Method:
GET - Path:
/api/v2/routing/queues/{queueId}/wrapupcodes - Headers:
Authorization: Bearer <token>,Accept: application/json - Response body:
{"wrapupCodes": [{"id": "abc123", "value": "follow-up", "type": "agent", "durationLimit": "PT1M", "default": false, "description": "Customer follow up", "enabled": true}]}
Step 2: Construct Update Payloads with Type Matrices and Duration Directives
Wrap-up codes require strict type classification and duration formatting. The routing engine accepts agent, customer, or both for the type field. Duration limits must follow ISO 8601 duration syntax. You will build the payload using SDK models to ensure type safety.
from platformclientv2.models import WrapupCode, QueueWrapupCodeUpdate
from datetime import timedelta
import re
VALID_TYPES = {"agent", "customer", "both"}
DURATION_REGEX = re.compile(r"^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$")
def parse_duration_to_iso(duration_seconds: int) -> str:
td = timedelta(seconds=duration_seconds)
hours, remainder = divmod(td.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
parts = []
if hours: parts.append(f"{hours}H")
if minutes: parts.append(f"{minutes}M")
if seconds: parts.append(f"{seconds}S")
return "PT" + "".join(parts) if parts else "PT0S"
def build_wrapup_code(value: str, code_type: str, duration_seconds: int, default: bool = False, description: str = "") -> WrapupCode:
if code_type not in VALID_TYPES:
raise ValueError(f"Invalid wrap-up code type '{code_type}'. Must be one of {VALID_TYPES}.")
iso_duration = parse_duration_to_iso(duration_seconds)
return WrapupCode(
value=value,
type=code_type,
duration_limit=iso_duration,
default=default,
description=description,
enabled=True
)
Step 3: Validate Schemas Against Routing Engine Constraints
The routing engine enforces a maximum of 50 wrap-up codes per queue. You must verify unique values, required fields, and duration format compliance before submission. This validation pipeline prevents atomic PUT failures and reduces API quota consumption.
def validate_wrapup_payload(codes: list) -> dict:
errors = []
values_seen = set()
if len(codes) > 50:
errors.append("Maximum wrap-up code count exceeded. Routing engine limit is 50.")
for idx, code in enumerate(codes):
if not code.value or not code.value.strip():
errors.append(f"Code at index {idx} has missing 'value' field.")
elif code.value in values_seen:
errors.append(f"Duplicate value '{code.value}' detected at index {idx}.")
else:
values_seen.add(code.value)
if not code.type or code.type not in VALID_TYPES:
errors.append(f"Code '{code.value}' has invalid or missing 'type'.")
if code.duration_limit and not DURATION_REGEX.match(code.duration_limit):
errors.append(f"Code '{code.value}' has invalid ISO 8601 duration format.")
return {"valid": len(errors) == 0, "errors": errors}
Step 4: Execute Atomic PUT and Handle Cache Invalidation
The update_queue_wrapup_codes endpoint performs an atomic replacement. All existing codes are removed and replaced with the submitted list. The Genesys Cloud platform automatically invalidates agent workspace caches upon successful PUT. You must implement retry logic for 429 rate limits and verify the 200 response.
import time
import logging
logger = logging.getLogger(__name__)
def update_queue_wrapup_codes(queue_api: QueueApi, queue_id: str, codes: list, max_retries: int = 3) -> dict:
body = QueueWrapupCodeUpdate(wrapup_codes=codes)
start_time = time.perf_counter()
for attempt in range(1, max_retries + 1):
try:
response = queue_api.update_queue_wrapup_codes(queue_id, body)
latency = time.perf_counter() - start_time
logger.info(f"Wrap-up codes updated successfully. Latency: {latency:.3f}s")
return {
"success": True,
"status_code": 200,
"latency_seconds": latency,
"code_count": len(codes),
"message": "Agent workspace cache invalidation triggered automatically by platform."
}
except ApiException as e:
if e.status == 429 and attempt < max_retries:
wait_time = 2 ** attempt
logger.warning(f"Rate limited (429). Retrying in {wait_time}s...")
time.sleep(wait_time)
continue
elif e.status == 409:
raise RuntimeError("Configuration conflict. Another process modified the queue. Retry with fresh fetch.")
elif e.status == 400:
raise RuntimeError(f"Schema validation failed: {e.body}")
else:
raise RuntimeError(f"PUT failed with status {e.status}: {e.body}")
raise RuntimeError("Max retries exceeded for 429 rate limit.")
HTTP equivalent for update:
- Method:
PUT - Path:
/api/v2/routing/queues/{queueId}/wrapupcodes - Headers:
Authorization: Bearer <token>,Content-Type: application/json,Accept: application/json - Request body:
{"wrapupCodes": [{"value": "callback", "type": "agent", "durationLimit": "PT2M", "default": false, "description": "Schedule callback", "enabled": true}]} - Response body:
{"wrapupCodes": [{"id": "def456", "value": "callback", "type": "agent", "durationLimit": "PT2M", "default": false, "description": "Schedule callback", "enabled": true}]}
Step 5: Synchronize with External WFM Tools via Webhook
After successful update, you must dispatch a synchronization event to external workforce management systems. This ensures alignment between routing configuration and scheduling engines. You will include latency metrics and audit metadata in the payload.
def dispatch_wfm_webhook(webhook_url: str, queue_id: str, result: dict, codes: list) -> bool:
payload = {
"event": "routing.wrapup_codes.updated",
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"queue_id": queue_id,
"metadata": {
"latency_seconds": result["latency_seconds"],
"code_count": result["code_count"],
"values": [c.value for c in codes]
}
}
try:
res = requests.post(webhook_url, json=payload, timeout=10)
res.raise_for_status()
logger.info(f"WFM webhook dispatched successfully to {webhook_url}")
return True
except requests.exceptions.RequestException as e:
logger.error(f"WFM webhook failed: {e}")
return False
Complete Working Example
The following module combines all components into a production-ready updater. It exposes a single entry point for automated routing management pipelines.
import os
import time
import logging
import requests
from platformclientv2 import PureCloudPlatformClientV2, QueueApi
from platformclientv2.rest import ApiException
from platformclientv2.models import WrapupCode, QueueWrapupCodeUpdate
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
class WrapupCodeUpdater:
def __init__(self, client_id: str, client_secret: str, webhook_url: str = None):
self.client_id = client_id
self.client_secret = client_secret
self.webhook_url = webhook_url
self.client = PureCloudPlatformClientV2()
self.queue_api = QueueApi(self.client)
self._authenticate()
def _authenticate(self):
try:
self.client.login_client_credential(self.client_id, self.client_secret)
except ApiException as e:
raise RuntimeError(f"Authentication failed: {e.status} {e.body}")
def update_queue_codes(self, queue_id: str, new_codes: list) -> dict:
logger.info(f"Processing update for queue {queue_id}")
existing = self.fetch_existing_codes(queue_id)
logger.info(f"Fetched {len(existing)} existing codes.")
wrapped_codes = []
for spec in new_codes:
wrapped_codes.append(self.build_code(spec))
validation = self.validate_payload(wrapped_codes)
if not validation["valid"]:
raise ValueError(f"Validation failed: {', '.join(validation['errors'])}")
result = self.execute_atomic_put(queue_id, wrapped_codes)
if self.webhook_url:
self.dispatch_webhook(queue_id, result, wrapped_codes)
return result
def fetch_existing_codes(self, queue_id: str) -> list:
try:
resp = self.queue_api.get_queue_wrapup_codes(queue_id)
return resp.wrapup_codes or []
except ApiException as e:
raise RuntimeError(f"Fetch failed: {e.status} {e.body}")
def build_code(self, spec: dict) -> WrapupCode:
duration_iso = self.to_iso_duration(spec.get("duration_seconds", 60))
return WrapupCode(
value=spec["value"],
type=spec["type"],
duration_limit=duration_iso,
default=spec.get("default", False),
description=spec.get("description", ""),
enabled=True
)
def to_iso_duration(self, seconds: int) -> str:
from datetime import timedelta
td = timedelta(seconds=seconds)
h, rem = divmod(td.seconds, 3600)
m, s = divmod(rem, 60)
return f"PT{h}H{m}M{s}S" if h else f"PT{m}M{s}S" if m else f"PT{s}S"
def validate_payload(self, codes: list) -> dict:
errors = []
seen = set()
if len(codes) > 50:
errors.append("Exceeds maximum code count of 50.")
for c in codes:
if not c.value or c.value in seen:
errors.append(f"Missing or duplicate value: {c.value}")
seen.add(c.value)
if c.type not in {"agent", "customer", "both"}:
errors.append(f"Invalid type: {c.type}")
return {"valid": len(errors) == 0, "errors": errors}
def execute_atomic_put(self, queue_id: str, codes: list) -> dict:
body = QueueWrapupCodeUpdate(wrapup_codes=codes)
start = time.perf_counter()
for attempt in range(1, 4):
try:
self.queue_api.update_queue_wrapup_codes(queue_id, body)
return {
"success": True,
"latency_seconds": time.perf_counter() - start,
"code_count": len(codes),
"message": "Update complete. Platform cache invalidation triggered."
}
except ApiException as e:
if e.status == 429 and attempt < 3:
time.sleep(2 ** attempt)
continue
raise RuntimeError(f"PUT failed: {e.status} {e.body}")
def dispatch_webhook(self, queue_id: str, result: dict, codes: list):
payload = {
"event": "routing.wrapup_codes.updated",
"queue_id": queue_id,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"latency_seconds": result["latency_seconds"],
"applied_codes": [c.value for c in codes]
}
try:
requests.post(self.webhook_url, json=payload, timeout=10).raise_for_status()
except Exception as e:
logger.error(f"Webhook sync failed: {e}")
if __name__ == "__main__":
updater = WrapupCodeUpdater(
client_id=os.environ["GENESYS_CLIENT_ID"],
client_secret=os.environ["GENESYS_CLIENT_SECRET"],
webhook_url=os.environ.get("WFM_WEBHOOK_URL")
)
new_config = [
{"value": "callback", "type": "agent", "duration_seconds": 120, "default": False, "description": "Schedule callback"},
{"value": "transfer", "type": "both", "duration_seconds": 60, "default": False, "description": "Transfer to specialist"},
{"value": "resolved", "type": "customer", "duration_seconds": 0, "default": True, "description": "Issue resolved"}
]
result = updater.update_queue_codes("a1b2c3d4-e5f6-7890-abcd-ef1234567890", new_config)
print(f"Update complete: {result}")
Common Errors & Debugging
Error: 400 Bad Request
- Cause: The payload violates routing engine schema constraints. Common triggers include missing
valuefields, invalidtypeenumeration, or malformed ISO 8601 duration strings. - Fix: Run the validation pipeline before submission. Verify
duration_limitmatchesPT#H#M#Sformat. Ensure no duplicatevaluestrings exist in the list. - Code showing the fix: The
validate_payloadmethod checks enumeration compliance and duplicate detection. Replace raw strings with SDKWrapupCodemodels to enforce type safety.
Error: 401 Unauthorized / 403 Forbidden
- Cause: Expired OAuth token or missing
routing:queue:writescope on the confidential client. - Fix: Regenerate the client secret. Verify scope assignment in the Genesys Cloud admin console under Security > OAuth Clients. The SDK automatically refreshes tokens, but initial credential validation must pass.
- Code showing the fix: The
_authenticatemethod catchesApiExceptionand raises explicit runtime errors. Addrouting:queue:readandrouting:queue:writeto the client scope list.
Error: 409 Conflict
- Cause: Another process modified the queue configuration between your fetch and PUT operations. The routing engine enforces optimistic concurrency.
- Fix: Implement a retry loop that re-fetches the current state, merges your changes, and re-submits. Never cache queue configurations longer than 30 seconds in automated pipelines.
- Code showing the fix: Wrap the
execute_atomic_putcall in a retry decorator. On 409, callfetch_existing_codesagain, rebuild the payload, and retry.
Error: 429 Too Many Requests
- Cause: Exceeded API rate limits. The routing endpoints enforce per-tenant and per-client throttling.
- Fix: Implement exponential backoff. The complete example includes a retry loop with
time.sleep(2 ** attempt). Distribute bulk updates across multiple seconds rather than executing synchronous bursts. - Code showing the fix: The
execute_atomic_putmethod catchesApiExceptionwith status 429 and sleeps before retrying. MonitorX-RateLimit-Resetheaders in raw HTTP responses for precise backoff timing.