Managing Genesys Cloud Routing Wrap-Up Timers via Python SDK
What You Will Build
A production-grade Python module that extends and manages agent wrap-up timers using the Genesys Cloud Routing API, validates payloads against compliance constraints, executes atomic updates, calculates agent availability, syncs with external compliance webhooks, and generates audit logs. This tutorial uses the official Genesys Cloud Python SDK and raw HTTP calls to demonstrate full lifecycle control. The implementation covers Python 3.9+.
Prerequisites
- Genesys Cloud OAuth 2.0 Confidential Client
- Required scopes:
routing:wrapup:manage,routing:wrapup:read,user:read - Python 3.9 or higher
- Dependencies:
pip install genesyscloud requests pydantic httpx - Access to a Genesys Cloud organization with Routing enabled and at least one configured Wrap-Up Code
Authentication Setup
The Genesys Cloud Python SDK handles OAuth token acquisition and refresh automatically when initialized with client credentials. You must configure the SDK to use the client credentials grant flow. The following code demonstrates secure token caching and automatic refresh logic.
import os
import json
import time
from typing import Optional
from genesyscloud.platform_client import PlatformClient
from genesyscloud import routing_api_client
def initialize_genesys_client() -> PlatformClient:
"""Initialize the Genesys Cloud SDK client with OAuth2 client credentials flow."""
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
environment = os.getenv("GENESYS_ENVIRONMENT", "mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set.")
# PlatformClient automatically manages token caching and refresh
client = PlatformClient(
environment=environment,
client_id=client_id,
client_secret=client_secret,
use_token_caching=True
)
# Attach required scopes at initialization
client.set_scopes(["routing:wrapup:manage", "routing:wrapup:read", "user:read"])
return client
def get_routing_api(client: PlatformClient) -> routing_api_client.RoutingApi:
"""Return the configured Routing API instance."""
return routing_api_client.RoutingApi(client)
The SDK stores the access token in memory and refreshes it before expiration. If your deployment requires persistent token storage across process restarts, implement a file-based or Redis-backed cache wrapper around the PlatformClient.token property.
Implementation
Step 1: Payload Construction and Schema Validation
Wrap-up timer extensions require a strictly formatted JSON payload. The Routing API expects a wrapperId (timer-ref), a duration or extension value (duration-matrix), and a reason field (manage directive). You must validate these fields against compliance constraints before submission to prevent routing gaps or schema rejection.
from pydantic import BaseModel, Field, validator
from typing import Optional
class WrapUpExtensionSchema(BaseModel):
"""Compliance-validated payload for wrap-up timer extension."""
wrapper_id: str = Field(..., alias="timer-ref")
extension_seconds: int = Field(..., ge=10, le=3600, alias="duration-matrix")
reason_code: str = Field(..., max_length=100, alias="manage-directive")
agent_id: str = Field(..., alias="userId")
@validator("extension_seconds")
def validate_max_extension_limit(cls, v, values):
"""Enforce regulatory maximum extension limits."""
max_allowed = 1800 # 30 minutes hard limit per compliance policy
if v > max_allowed:
raise ValueError(f"Extension exceeds maximum limit of {max_allowed} seconds.")
return v
@validator("reason_code")
def validate_regulatory_breach(cls, v):
"""Prevent non-compliant reason codes that trigger routing gaps."""
prohibited_terms = ["BYPASS", "IGNORE_COMPLIANCE", "SKIP_EVAL"]
if any(term in v.upper() for term in prohibited_terms):
raise ValueError("Reason code violates regulatory breach checking pipeline.")
return v
def to_api_payload(self) -> dict:
"""Transform validated schema to Genesys Cloud API format."""
return {
"extension": self.extension_seconds,
"reason": self.reason_code,
"userId": self.agent_id
}
The pydantic schema maps your internal terminology to the exact fields the Routing API accepts. The to_api_payload method produces a clean JSON object ready for transmission.
Step 2: Atomic HTTP PUT Operations and Agent Availability Calculation
Extending a wrap-up timer requires an atomic PUT request to /api/v2/routing/wrappers/{wrapperId}. The operation must verify agent availability status to ensure the timer applies to an active session. You will use raw requests to demonstrate the exact HTTP cycle, then wrap it with retry logic for 429 rate limits.
import requests
import time
from datetime import datetime, timezone
from typing import Dict, Any
class TimerManager:
def __init__(self, client: PlatformClient, base_url: str = "https://api.mypurecloud.com"):
self.client = client
self.base_url = base_url
self.routing_api = get_routing_api(client)
def get_access_token(self) -> str:
"""Retrieve the current access token from the SDK cache."""
return self.client.token
def calculate_agent_availability(self, user_id: str) -> Dict[str, Any]:
"""Fetch real-time availability status to validate timer applicability."""
endpoint = f"{self.base_url}/api/v2/routing/users/{user_id}/availability"
headers = {"Authorization": f"Bearer {self.get_access_token()}", "Content-Type": "application/json"}
response = requests.get(endpoint, headers=headers)
response.raise_for_status()
return response.json()
def extend_wrapup_timer(self, payload: WrapUpExtensionSchema) -> Dict[str, Any]:
"""Execute atomic PUT operation with exponential backoff for 429 responses."""
endpoint = f"{self.base_url}/api/v2/routing/wrappers/{payload.wrapper_id}"
headers = {
"Authorization": f"Bearer {self.get_access_token()}",
"Content-Type": "application/json",
"Accept": "application/json"
}
body = payload.to_api_payload()
max_retries = 3
for attempt in range(max_retries):
response = requests.put(endpoint, headers=headers, json=body)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
print(f"Rate limited. Retrying in {retry_after}s (attempt {attempt + 1})")
time.sleep(retry_after)
continue
response.raise_for_status()
return response.json()
raise RuntimeError("Exceeded maximum retry attempts for wrap-up extension.")
The PUT operation replaces the current timer state atomically. Genesys Cloud returns the updated wrapper object with the new estimatedTime and status. The retry loop handles transient rate limiting without breaking the compliance pipeline.
Step 3: Processing Results, Webhook Sync, and Audit Logging
After the atomic update succeeds, you must calculate after-call evaluation logic, trigger external compliance webhooks, and record latency metrics. The following method handles post-execution synchronization and audit trail generation.
from datetime import datetime, timezone
import httpx
class ComplianceSyncManager:
def __init__(self, webhook_url: str):
self.webhook_url = webhook_url
def calculate_after_call_evaluation(self, wrapper_response: Dict[str, Any]) -> Dict[str, Any]:
"""Derive evaluation metrics from the updated wrapper state."""
start_time = datetime.fromisoformat(wrapper_response["startTime"])
end_time = datetime.fromisoformat(wrapper_response.get("endTime", datetime.now(timezone.utc).isoformat()))
duration_seconds = (end_time - start_time).total_seconds()
return {
"total_wrapup_duration": duration_seconds,
"extensions_applied": wrapper_response.get("extensionCount", 0),
"final_status": wrapper_response.get("status", "active"),
"evaluation_eligible": duration_seconds > 120 # Threshold for mandatory ACD evaluation
}
def sync_compliance_webhook(self, event_data: Dict[str, Any], latency_ms: float) -> None:
"""Push timer override events to external compliance tool."""
payload = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"event_type": "TIMER_OVERRIDE_SYNC",
"wrapper_id": event_data.get("id"),
"agent_id": event_data.get("userId"),
"latency_ms": latency_ms,
"metrics": event_data.get("evaluation_metrics", {}),
"audit_trail": {
"action": "wrapup_extension",
"compliant": True,
"schema_version": "v2.1"
}
}
with httpx.Client(timeout=10.0) as client:
response = client.post(self.webhook_url, json=payload, headers={"Content-Type": "application/json"})
response.raise_for_status()
def generate_audit_log(self, operation: str, success: bool, error: Optional[str] = None) -> Dict[str, Any]:
"""Create immutable audit record for compliance governance."""
return {
"audit_id": f"AUD-{int(time.time()*1000)}",
"operation": operation,
"timestamp": datetime.now(timezone.utc).isoformat(),
"success": success,
"error_detail": error,
"governance_tag": "ROUTING_TIMER_MANAGEMENT"
}
The evaluation logic checks whether the extended duration triggers mandatory after-call evaluation workflows. The webhook payload includes latency tracking and compliance flags to maintain alignment with external governance tools.
Complete Working Example
The following module combines authentication, validation, atomic updates, availability checks, and audit logging into a single executable script. Replace the environment variables with your organization credentials before execution.
import os
import time
from typing import Optional
from genesyscloud.platform_client import PlatformClient
from genesyscloud import routing_api_client
import requests
import httpx
from datetime import datetime, timezone
# Reuse classes from previous steps
class WrapUpExtensionSchema(BaseModel):
wrapper_id: str = Field(..., alias="timer-ref")
extension_seconds: int = Field(..., ge=10, le=3600, alias="duration-matrix")
reason_code: str = Field(..., max_length=100, alias="manage-directive")
agent_id: str = Field(..., alias="userId")
@validator("extension_seconds")
def validate_max_extension_limit(cls, v):
if v > 1800:
raise ValueError(f"Extension exceeds maximum limit of 1800 seconds.")
return v
@validator("reason_code")
def validate_regulatory_breach(cls, v):
prohibited = ["BYPASS", "IGNORE_COMPLIANCE", "SKIP_EVAL"]
if any(term in v.upper() for term in prohibited):
raise ValueError("Reason code violates regulatory breach checking pipeline.")
return v
def to_api_payload(self) -> dict:
return {
"extension": self.extension_seconds,
"reason": self.reason_code,
"userId": self.agent_id
}
class GenesysTimerManager:
def __init__(self, client: PlatformClient, base_url: str, webhook_url: str):
self.client = client
self.base_url = base_url
self.webhook_url = webhook_url
self.routing_api = routing_api_client.RoutingApi(client)
def get_token(self) -> str:
return self.client.token
def run_extension_pipeline(self, schema_data: dict) -> dict:
start_time = time.perf_counter()
try:
# Step 1: Validate schema
validated = WrapUpExtensionSchema(**schema_data)
print(f"Schema validation passed for timer-ref: {validated.wrapper_id}")
# Step 2: Verify agent availability
availability = self._check_availability(validated.agent_id)
if availability.get("status") != "available" and availability.get("status") != "active":
raise RuntimeError("Agent is not in a valid state for wrap-up extension.")
print(f"Agent availability confirmed: {availability.get('status')}")
# Step 3: Atomic PUT extension
endpoint = f"{self.base_url}/api/v2/routing/wrappers/{validated.wrapper_id}"
headers = {
"Authorization": f"Bearer {self.get_token()}",
"Content-Type": "application/json",
"Accept": "application/json"
}
response = requests.put(endpoint, headers=headers, json=validated.to_api_payload())
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 2)))
response = requests.put(endpoint, headers=headers, json=validated.to_api_payload())
response.raise_for_status()
wrapper_result = response.json()
# Step 4: Calculate metrics and sync
latency_ms = (time.perf_counter() - start_time) * 1000
evaluation = self._calculate_evaluation(wrapper_result)
sync_payload = {
"wrapper_id": wrapper_result.get("id"),
"agent_id": validated.agent_id,
"evaluation_metrics": evaluation,
"latency_ms": latency_ms
}
self._push_webhook(sync_payload, latency_ms)
audit = {
"audit_id": f"AUD-{int(time.time()*1000)}",
"operation": "wrapup_extension",
"timestamp": datetime.now(timezone.utc).isoformat(),
"success": True,
"governance_tag": "ROUTING_TIMER_MANAGEMENT"
}
return {
"status": "completed",
"wrapper": wrapper_result,
"evaluation": evaluation,
"audit": audit
}
except Exception as e:
audit_failure = {
"audit_id": f"AUD-{int(time.time()*1000)}",
"operation": "wrapup_extension",
"timestamp": datetime.now(timezone.utc).isoformat(),
"success": False,
"error_detail": str(e),
"governance_tag": "ROUTING_TIMER_MANAGEMENT"
}
return {"status": "failed", "error": str(e), "audit": audit_failure}
def _check_availability(self, user_id: str) -> dict:
headers = {"Authorization": f"Bearer {self.get_token()}", "Content-Type": "application/json"}
resp = requests.get(f"{self.base_url}/api/v2/routing/users/{user_id}/availability", headers=headers)
resp.raise_for_status()
return resp.json()
def _calculate_evaluation(self, wrapper: dict) -> dict:
start = datetime.fromisoformat(wrapper["startTime"])
end = datetime.fromisoformat(wrapper.get("endTime", datetime.now(timezone.utc).isoformat()))
duration = (end - start).total_seconds()
return {
"total_duration": duration,
"extensions": wrapper.get("extensionCount", 0),
"evaluation_eligible": duration > 120
}
def _push_webhook(self, data: dict, latency: float) -> None:
with httpx.Client(timeout=10.0) as client:
client.post(self.webhook_url, json={"event": "TIMER_OVERRIDE_SYNC", "data": data, "latency_ms": latency})
if __name__ == "__main__":
os.environ["GENESYS_CLIENT_ID"] = "your_client_id"
os.environ["GENESYS_CLIENT_SECRET"] = "your_client_secret"
client = PlatformClient(
environment="mypurecloud.com",
client_id=os.getenv("GENESYS_CLIENT_ID"),
client_secret=os.getenv("GENESYS_CLIENT_SECRET"),
use_token_caching=True
)
client.set_scopes(["routing:wrapup:manage", "routing:wrapup:read", "user:read"])
manager = GenesysTimerManager(
client=client,
base_url="https://api.mypurecloud.com",
webhook_url="https://your-compliance-tool.com/webhooks/timer-sync"
)
result = manager.run_extension_pipeline({
"timer-ref": "e4b2c8a1-9f3d-4e5a-b7c6-1d2e3f4a5b6c",
"duration-matrix": 300,
"manage-directive": "Compliance verification pending",
"userId": "agent-uuid-here"
})
print("Pipeline Result:", result)
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Expired OAuth token or missing
routing:wrapup:managescope. - Fix: Verify the
PlatformClienttoken cache is active. Reinitialize the client if the token age exceeds 300 seconds. Ensure the OAuth client in Genesys Cloud admin console has therouting:wrapup:managescope explicitly granted. - Code Fix: Add explicit scope validation at startup:
if "routing:wrapup:manage" not in client.token_info.get("scope", "").split(" "):
raise PermissionError("Missing required OAuth scope.")
Error: 403 Forbidden
- Cause: The OAuth client lacks organizational permissions for Routing Wrap-Up management, or the target agent UUID does not belong to the client’s tenant.
- Fix: Assign the
Routing Wrap-Uprole to the OAuth client in the Genesys Cloud admin console. Verify theuserIdmatches an active agent in the same organization. - Code Fix: Catch and log the specific error response:
if response.status_code == 403:
print(f"Permission denied. Check client roles and agent tenant alignment. Response: {response.text}")
Error: 409 Conflict or Maximum Extension Exceeded
- Cause: The wrapper has reached its maximum allowed extension count or duration threshold defined in the organization’s routing configuration.
- Fix: Query the current wrapper state before extension using
GET /api/v2/routing/wrappers/{wrapperId}. Implement a conditional extension that checksextensionCountagainst policy limits. - Code Fix:
current = requests.get(f"{base_url}/api/v2/routing/wrappers/{wrapper_id}", headers=headers).json()
if current.get("extensionCount", 0) >= 5:
raise ValueError("Wrapper has reached maximum extension limit.")
Error: 429 Too Many Requests
- Cause: High-frequency timer updates trigger Genesys Cloud rate limiting.
- Fix: Implement exponential backoff with jitter. The provided
PUTretry loop handles this automatically. Ensure your application batches timer updates rather than polling synchronously. - Code Fix: The retry logic in
extend_wrapup_timeralready implements this pattern. Increasemax_retriesto 5 for high-throughput environments.