Automating Wrap-Up Code Assignment Based on Post-Call Survey Results Using the Genesys Cloud Interaction API and Python
What You Will Build
- A Python script that processes post-call survey responses and automatically assigns the corresponding wrap-up code to a Genesys Cloud interaction.
- This implementation uses the Genesys Cloud Interaction API (
/api/v2/interactions/events) and the official Python SDK. - The tutorial covers Python 3.9+ with type hints, JWT authentication, and production-grade error handling.
Prerequisites
- OAuth Client Type: Confidential Client (Server-to-Server) with JWT grant type enabled.
- Required Scopes:
interaction:write,interaction:view - SDK Version:
genesys-cloud-purecloud-platform-clientv130.0.0+ - Runtime: Python 3.9+
- Dependencies:
httpx>=0.24.0,pyjwt>=2.8.0,genesys-cloud-purecloud-platform-client>=130.0.0
Authentication Setup
Genesys Cloud server-to-server integrations require a JWT grant flow. The following code generates a signed JWT, exchanges it for an access token, and implements token caching with automatic refresh logic.
import time
import httpx
from datetime import datetime, timezone, timedelta
import jwt
from typing import Optional, Dict, Any
class GenesysAuth:
def __init__(self, org_id: str, client_id: str, private_key_pem: str, env: str = "mypurecloud.com"):
self.org_id = org_id
self.client_id = client_id
self.private_key = private_key_pem
self.base_url = f"https://api.{env}"
self.token_url = f"{self.base_url}/oauth/token"
self.access_token: Optional[str] = None
self.token_expires_at: Optional[float] = None
def _generate_jwt(self) -> str:
now = datetime.now(timezone.utc)
payload = {
"iss": self.client_id,
"sub": self.client_id,
"aud": self.token_url,
"iat": now,
"exp": now + timedelta(minutes=10),
"jti": str(time.time())
}
return jwt.encode(payload, self.private_key, algorithm="RS256")
def get_access_token(self) -> str:
if self.access_token and self.token_expires_at and time.time() < self.token_expires_at - 60:
return self.access_token
jwt_token = self._generate_jwt()
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": jwt_token
}
response = httpx.post(self.token_url, headers=headers, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expires_at = time.time() + token_data["expires_in"] - 60
return self.access_token
The get_access_token() method caches the token and refreshes it 60 seconds before expiration. This prevents unnecessary OAuth calls during high-throughput survey processing.
Implementation
Step 1: Map Survey Results to Wrap-Up Code IDs
Wrap-up codes in Genesys Cloud are identified by their internal UUID. You must retrieve these IDs once and cache them. The following function queries the wrap-up code definitions and builds a lookup dictionary.
from genesyscloud.platform_client import PureCloudPlatformClientV2
from genesyscloud.wrapup_code.api import WrapupCodeApi
def load_wrapup_codes(platform_client: PureCloudPlatformClientV2, division_id: str) -> Dict[str, str]:
"""
Retrieves all wrap-up codes for a division and returns a mapping of code strings to UUIDs.
Scope required: interaction:view
"""
wrapup_api = WrapupCodeApi(platform_client)
code_map = {}
# Pagination handling for wrap-up codes
page_size = 25
page_number = 1
while True:
try:
response = wrapup_api.get_wrapupcodes(
division_id=division_id,
page_size=page_size,
page_number=page_number
)
if not response.entities:
break
for code in response.entities:
if code.code:
code_map[code.code] = code.id
if page_number >= response.page_count:
break
page_number += 1
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
retry_after = int(e.response.headers.get("Retry-After", 5))
time.sleep(retry_after)
continue
raise
return code_map
This function handles pagination and implements retry logic for 429 Too Many Requests responses. The Retry-After header dictates the sleep duration. If the header is missing, the script defaults to 5 seconds.
Step 2: Process Survey Payload and Determine Wrap-Up
Post-call surveys typically arrive via webhook or queue consumer. The payload contains an interactionId and response scores. The mapping logic translates survey outcomes into the corresponding wrap-up code string.
from typing import Optional
def determine_wrapup_code(survey_payload: Dict[str, Any], code_map: Dict[str, str]) -> Optional[str]:
"""
Analyzes survey scores and returns the corresponding wrap-up code UUID.
Returns None if no wrap-up should be assigned.
"""
csat_score = survey_payload.get("csat_score", 0)
nps_score = survey_payload.get("nps_score", 0)
issue_type = survey_payload.get("issue_type", "").upper()
# Business logic mapping
if nps_score <= 6:
target_code = "WU_PROMPT_ESCALATION"
elif nps_score >= 9 and csat_score >= 4:
target_code = "WU_PROMPT_SUCCESS"
elif issue_type == "TECHNICAL":
target_code = "WU_PROMPT_TECH_ISSUE"
else:
target_code = "WU_PROMPT_GENERAL"
return code_map.get(target_code)
The function returns the UUID string. If the business logic does not match a known code, the function returns None, preventing invalid API calls.
Step 3: Assign Wrap-Up Code via Interaction API
The Interaction API accepts wrap-up events through POST /api/v2/interactions/events. The SDK method post_interactions_events constructs the request. You must include the interactionId, eventType, and wrapUpCode object.
from genesyscloud.interaction.api import InteractionApi
from genesyscloud.model.post_interactions_events_request import PostInteractionsEventsRequest
from genesyscloud.model.wrapup_code import WrapupCode
def assign_wrapup_code(
platform_client: PureCloudPlatformClientV2,
interaction_id: str,
wrapup_code_id: str,
wrapup_time: Optional[str] = None
) -> Dict[str, Any]:
"""
Assigns a wrap-up code to a completed interaction.
Scope required: interaction:write
"""
interaction_api = InteractionApi(platform_client)
if not wrapup_time:
wrapup_time = datetime.now(timezone.utc).isoformat()
wrapup_code_obj = WrapupCode(id=wrapup_code_id, code=None)
event_request = PostInteractionsEventsRequest(
event_type="wrapup",
interaction_id=interaction_id,
wrap_up_code=wrapup_code_obj,
wrap_up_time=wrapup_time
)
try:
response = interaction_api.post_interactions_events(event_request)
return response.to_dict()
except httpx.HTTPStatusError as e:
status = e.response.status_code
if status == 429:
retry_after = int(e.response.headers.get("Retry-After", 5))
time.sleep(retry_after)
# Retry once after rate limit
return assign_wrapup_code(platform_client, interaction_id, wrapup_code_id, wrapup_time)
elif status == 404:
raise ValueError(f"Interaction ID {interaction_id} not found")
elif status == 400:
raise ValueError(f"Invalid wrap-up code or interaction state: {e.response.text}")
else:
raise
The PostInteractionsEventsRequest object serializes to the exact JSON payload expected by the API. The wrapup event type transitions the interaction to a wrapped state and attaches the code. The retry logic handles transient rate limits without blocking the queue consumer.
Complete Working Example
The following script combines authentication, lookup, mapping, and assignment into a single executable module. Replace the placeholder credentials and division ID before execution.
import time
import httpx
from datetime import datetime, timezone, timedelta
import jwt
from typing import Optional, Dict, Any
from genesyscloud.platform_client import PureCloudPlatformClientV2
from genesyscloud.wrapup_code.api import WrapupCodeApi
from genesyscloud.interaction.api import InteractionApi
from genesyscloud.model.post_interactions_events_request import PostInteractionsEventsRequest
from genesyscloud.model.wrapup_code import WrapupCode
class GenesysAuth:
def __init__(self, org_id: str, client_id: str, private_key_pem: str, env: str = "mypurecloud.com"):
self.org_id = org_id
self.client_id = client_id
self.private_key = private_key_pem
self.base_url = f"https://api.{env}"
self.token_url = f"{self.base_url}/oauth/token"
self.access_token: Optional[str] = None
self.token_expires_at: Optional[float] = None
def _generate_jwt(self) -> str:
now = datetime.now(timezone.utc)
payload = {
"iss": self.client_id,
"sub": self.client_id,
"aud": self.token_url,
"iat": now,
"exp": now + timedelta(minutes=10),
"jti": str(time.time())
}
return jwt.encode(payload, self.private_key, algorithm="RS256")
def get_access_token(self) -> str:
if self.access_token and self.token_expires_at and time.time() < self.token_expires_at - 60:
return self.access_token
jwt_token = self._generate_jwt()
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": jwt_token
}
response = httpx.post(self.token_url, headers=headers, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expires_at = time.time() + token_data["expires_in"] - 60
return self.access_token
def load_wrapup_codes(platform_client: PureCloudPlatformClientV2, division_id: str) -> Dict[str, str]:
wrapup_api = WrapupCodeApi(platform_client)
code_map = {}
page_size = 25
page_number = 1
while True:
try:
response = wrapup_api.get_wrapupcodes(
division_id=division_id,
page_size=page_size,
page_number=page_number
)
if not response.entities:
break
for code in response.entities:
if code.code:
code_map[code.code] = code.id
if page_number >= response.page_count:
break
page_number += 1
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
retry_after = int(e.response.headers.get("Retry-After", 5))
time.sleep(retry_after)
continue
raise
return code_map
def determine_wrapup_code(survey_payload: Dict[str, Any], code_map: Dict[str, str]) -> Optional[str]:
csat_score = survey_payload.get("csat_score", 0)
nps_score = survey_payload.get("nps_score", 0)
issue_type = survey_payload.get("issue_type", "").upper()
if nps_score <= 6:
target_code = "WU_PROMPT_ESCALATION"
elif nps_score >= 9 and csat_score >= 4:
target_code = "WU_PROMPT_SUCCESS"
elif issue_type == "TECHNICAL":
target_code = "WU_PROMPT_TECH_ISSUE"
else:
target_code = "WU_PROMPT_GENERAL"
return code_map.get(target_code)
def assign_wrapup_code(
platform_client: PureCloudPlatformClientV2,
interaction_id: str,
wrapup_code_id: str,
wrapup_time: Optional[str] = None
) -> Dict[str, Any]:
interaction_api = InteractionApi(platform_client)
if not wrapup_time:
wrapup_time = datetime.now(timezone.utc).isoformat()
wrapup_code_obj = WrapupCode(id=wrapup_code_id, code=None)
event_request = PostInteractionsEventsRequest(
event_type="wrapup",
interaction_id=interaction_id,
wrap_up_code=wrapup_code_obj,
wrap_up_time=wrapup_time
)
try:
response = interaction_api.post_interactions_events(event_request)
return response.to_dict()
except httpx.HTTPStatusError as e:
status = e.response.status_code
if status == 429:
retry_after = int(e.response.headers.get("Retry-After", 5))
time.sleep(retry_after)
return assign_wrapup_code(platform_client, interaction_id, wrapup_code_id, wrapup_time)
elif status == 404:
raise ValueError(f"Interaction ID {interaction_id} not found")
elif status == 400:
raise ValueError(f"Invalid wrap-up code or interaction state: {e.response.text}")
else:
raise
def process_survey(survey_payload: Dict[str, Any], code_map: Dict[str, str], platform_client: PureCloudPlatformClientV2) -> None:
interaction_id = survey_payload.get("interactionId")
if not interaction_id:
raise ValueError("Survey payload missing interactionId")
wrapup_code_id = determine_wrapup_code(survey_payload, code_map)
if not wrapup_code_id:
print("No matching wrap-up code found for survey results.")
return
result = assign_wrapup_code(platform_client, interaction_id, wrapup_code_id)
print(f"Wrap-up assigned successfully. Event ID: {result.get('eventId')}")
if __name__ == "__main__":
# Configuration
ORG_ID = "your-org-id"
CLIENT_ID = "your-client-id"
PRIVATE_KEY = """-----BEGIN PRIVATE KEY-----
YOUR_PEM_PRIVATE_KEY_HERE
-----END PRIVATE KEY-----"""
ENVIRONMENT = "mypurecloud.com"
DIVISION_ID = "your-division-id"
# Sample survey payload
SAMPLE_SURVEY = {
"interactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"csat_score": 5,
"nps_score": 10,
"issue_type": "billing"
}
# Initialize
auth = GenesysAuth(ORG_ID, CLIENT_ID, PRIVATE_KEY, ENVIRONMENT)
client = PureCloudPlatformClientV2.create(auth.get_access_token, ORG_ID, ENVIRONMENT)
# Load codes
code_map = load_wrapup_codes(client, DIVISION_ID)
# Process
process_survey(SAMPLE_SURVEY, code_map, client)
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The JWT signature is invalid, the client ID does not match the registered OAuth client, or the access token has expired.
- Fix: Verify the private key matches the public key uploaded to the Genesys Cloud OAuth client configuration. Ensure the
audclaim in the JWT exactly matches the token endpoint URL. Check that the system clock is synchronized with NTP servers. - Code Fix: The
GenesysAuthclass automatically refreshes tokens before expiration. If the error persists, log the JWT payload before signing to validate claims.
Error: 403 Forbidden
- Cause: The OAuth client lacks the
interaction:writescope, or the client is restricted to a specific division that does not contain the target interaction. - Fix: Navigate to the OAuth client settings and add
interaction:writeto the scope list. Verify the division ID passed toload_wrapup_codesmatches the interaction division. - Code Fix: Inspect the token response. The
scopefield must containinteraction:write. If missing, update the client configuration and regenerate the token.
Error: 429 Too Many Requests
- Cause: The script exceeds the Genesys Cloud rate limit for the Interaction API (typically 100 requests per second per client).
- Fix: Implement exponential backoff and respect the
Retry-Afterheader. Theassign_wrapup_codefunction includes a single retry loop. For production queues, implement a token bucket or sliding window rate limiter. - Code Fix: The existing retry logic sleeps for the duration specified in
Retry-After. Increase the sleep duration or add jitter if cascading failures occur.
Error: 400 Bad Request
- Cause: The interaction is not in a
completedstate, the wrap-up code UUID does not exist, or thewrapUpTimeis invalid. - Fix: Verify the interaction status via
GET /api/v2/interactions/{interactionId}. Ensure the wrap-up code was successfully loaded from the correct division. Format timestamps in ISO 8601 with timezone designators. - Code Fix: Add a validation step before calling
post_interactions_eventsto check interaction state. Log the exactwrapup_code_idbeing sent to confirm it matches the division lookup.