Archiving Genesys Cloud Routing API Wrap-Up Codes via Java
What You Will Build
- A Java service that safely retires wrap-up skills by validating dependencies, enforcing retention constraints, and executing atomic PATCH operations.
- The implementation uses the Genesys Cloud Routing API (
/api/v2/routing/wrappingskills) and Analytics API (/api/v2/analytics/conversations/summary/query) via the official Java SDK. - The code is written in Java 17 and demonstrates production-grade error handling, 429 retry logic, webhook synchronization, and audit logging.
Prerequisites
- OAuth2 client credentials with the following scopes:
routing:wrapupskill:read,routing:wrapupskill:write,analytics:conversations:view,platform:webhook:write - Genesys Cloud Java SDK
v2.0.0+(Maven:com.mulesoft.weave:genesyscloud-java-sdk) - Java 17 or higher
- Dependencies:
jackson-databind,slf4j-api,httpclient5(included in SDK) - Access to a Genesys Cloud organization with routing and analytics permissions
Authentication Setup
Genesys Cloud uses OAuth2 client credentials flow for server-to-server integrations. The SDK provides a built-in OAuthClient that handles token acquisition and caching. You must initialize the platform client before invoking any Routing API methods.
import com.mulesoft.weave.sdk.platform.client.PureCloudPlatformClientV2;
import com.mulesoft.weave.sdk.platform.client.auth.OAuthClient;
import com.mulesoft.weave.sdk.platform.client.auth.OAuthResponse;
import com.mulesoft.weave.sdk.platform.client.ApiException;
import java.util.List;
public class GenesysAuth {
private static final String BASE_URL = "https://api.mypurecloud.com";
private static final String CLIENT_ID = "YOUR_CLIENT_ID";
private static final String CLIENT_SECRET = "YOUR_CLIENT_SECRET";
private static final List<String> REQUIRED_SCOPES = List.of(
"routing:wrapupskill:read",
"routing:wrapupskill:write",
"analytics:conversations:view",
"platform:webhook:write"
);
public static PureCloudPlatformClientV2 initializePlatformClient() throws ApiException {
var platformClient = PureCloudPlatformClientV2.create(BASE_URL);
var oauthClient = platformClient.getOAuthClient();
// Execute client credentials grant
OAuthResponse oauthResponse = oauthClient.clientCredentials(CLIENT_ID, CLIENT_SECRET, REQUIRED_SCOPES);
if (oauthResponse.getAccessToken() == null) {
throw new RuntimeException("OAuth token acquisition failed. Verify client credentials and scopes.");
}
// SDK automatically caches and refreshes tokens before expiration
return platformClient;
}
}
The SDK caches the access token and automatically appends the Authorization: Bearer <token> header to subsequent requests. If the token expires, the SDK triggers a silent refresh before the next API call.
Implementation
Step 1: Initialize SDK and Validate Wrap-Up Skill Schema
You must retrieve the wrap-up skill by its ID to verify the code-ref and category-matrix before attempting retirement. Genesys Cloud enforces strict schema validation on PATCH requests. You must verify that the code field matches the expected reference and that the category aligns with your routing matrix.
import com.mulesoft.weave.sdk.platform.client.api.RoutingApi;
import com.mulesoft.weave.sdk.platform.client.model.WrapupSkill;
public class WrapupSkillValidator {
private final RoutingApi routingApi;
public WrapupSkillValidator(PureCloudPlatformClientV2 platformClient) {
this.routingApi = new RoutingApi(platformClient);
}
public WrapupSkill validateAndFetch(String wrapupSkillId, String expectedCode, String expectedCategory) throws ApiException {
// Fetch current wrap-up skill state
WrapupSkill skill = routingApi.getRoutingWrapupskill(wrapupSkillId, null, null);
// Format verification and code-ref reference check
if (!skill.getCode().equals(expectedCode)) {
throw new IllegalArgumentException(String.format(
"Code mismatch. Expected: %s, Found: %s", expectedCode, skill.getCode()
));
}
// Category-matrix alignment check
if (!expectedCategory.equalsIgnoreCase(skill.getCategory())) {
throw new IllegalArgumentException(String.format(
"Category mismatch. Expected: %s, Found: %s", expectedCategory, skill.getCategory()
));
}
return skill;
}
}
Required OAuth Scope: routing:wrapupskill:read
Expected Response: Returns a WrapupSkill object containing id, code, category, enabled, selfUri, and version.
Step 2: Dependency Check and Usage-Stat Verification Pipeline
Before retiring a wrap-up skill, you must evaluate usage-stat and dependency-check logic. Genesys Cloud does not allow disabling a wrap-up skill if it is actively assigned to queues or has recent conversation data within your retention-constraints. You will query the Analytics API to verify zero usage in the retention window and check for active routing dependencies.
import com.mulesoft.weave.sdk.platform.client.api.AnalyticsApi;
import com.mulesoft.weave.sdk.platform.client.model.ConversationSummaryQuery;
import com.mulesoft.weave.sdk.platform.client.model.ConversationSummaryQueryResponse;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
public class UsageDependencyChecker {
private final AnalyticsApi analyticsApi;
private static final int RETENTION_WINDOW_DAYS = 90;
public UsageDependencyChecker(PureCloudPlatformClientV2 platformClient) {
this.analyticsApi = new AnalyticsApi(platformClient);
}
public boolean verifyZeroUsageAndDependencies(String wrapupSkillId) throws ApiException {
// Define retention-constraints window
Instant endTime = Instant.now();
Instant startTime = endTime.minus(RETENTION_WINDOW_DAYS, ChronoUnit.DAYS);
// Construct analytics query for usage-stat calculation
ConversationSummaryQuery query = new ConversationSummaryQuery();
query.setInterval(String.format("%s/%s", startTime.toString(), endTime.toString()));
query.setEntityId(wrapupSkillId);
query.setFilter("wrapupSkillId", wrapupSkillId);
ConversationSummaryQueryResponse response = analyticsApi.postAnalyticsConversationsSummaryQuery(query);
// Audit-retention verification pipeline: ensure no conversations exist in window
if (response.getTotalCount() != null && response.getTotalCount() > 0) {
throw new IllegalStateException(
String.format("Dependency check failed. Wrap-up skill %s has %d recent conversations. " +
"Cannot retire within retention-constraints window.", wrapupSkillId, response.getTotalCount())
);
}
// Active-task checking: verify no open interactions reference this skill
// In production, you would also query /api/v2/routing/queues to ensure no queue.assignments contain this skill
return true;
}
}
Required OAuth Scope: analytics:conversations:view
Error Handling: Throws IllegalStateException if usage exceeds zero. This prevents reporting gaps during Genesys Cloud scaling operations.
Step 3: Atomic HTTP PATCH Retire Operation with Disable Trigger
The retirement process uses an atomic HTTP PATCH operation to apply the retire directive. You will construct a WrapupSkillPatch payload that sets enabled to false. This acts as an automatic disable trigger. You must include the version field from the GET response to prevent concurrent modification conflicts. The code implements exponential backoff for 429 rate-limit cascades.
import com.mulesoft.weave.sdk.platform.client.model.WrapupSkillPatch;
import java.util.concurrent.TimeUnit;
public class WrapupSkillRetirer {
private final RoutingApi routingApi;
private static final int MAX_RETRIES = 3;
private static final long BASE_RETRY_DELAY_MS = 1000;
public WrapupSkillRetirer(PureCloudPlatformClientV2 platformClient) {
this.routingApi = new RoutingApi(platformClient);
}
public WrapupSkill retireWrapupSkill(String wrapupSkillId, int currentVersion) throws ApiException, InterruptedException {
// Construct archiving payloads with retire directive and automatic disable trigger
WrapupSkillPatch patchPayload = new WrapupSkillPatch();
patchPayload.setOp("replace");
patchPayload.setPath("/enabled");
patchPayload.setValue(false);
// Note: The SDK handles JSON serialization. For atomic PATCH, we use the update method.
// Genesys Cloud Routing API expects a WrapupSkill object for full update or patch for partial.
// We will use the SDK's update method which maps to PATCH /api/v2/routing/wrappingskills/{id}
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
var updatedSkill = routingApi.patchRoutingWrapupskill(
wrapupSkillId,
currentVersion,
patchPayload,
null,
null
);
return updatedSkill;
} catch (ApiException ex) {
if (ex.getCode() == 429 && attempt < MAX_RETRIES) {
long delay = BASE_RETRY_DELAY_MS * (long) Math.pow(2, attempt - 1);
Thread.sleep(delay);
} else {
throw ex;
}
}
}
throw new RuntimeException("Max retries exceeded for 429 rate limit");
}
}
Required OAuth Scope: routing:wrapupskill:write
HTTP Request/Response Cycle:
PATCH /api/v2/routing/wrappingskills/{id} HTTP/1.1
Host: api.mypurecloud.com
Authorization: Bearer <access_token>
Content-Type: application/json
Accept: application/json
{
"op": "replace",
"path": "/enabled",
"value": false
}
Response:
{
"id": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
"code": "CUSTOMER_REQUESTED_CANCEL",
"category": "Customer Requested",
"enabled": false,
"version": 4,
"selfUri": "/api/v2/routing/wrappingskills/a1b2c3d4-5678-90ef-ghij-klmnopqrstuv"
}
Step 4: Webhook Synchronization and Audit Logging
You must synchronize archiving events with your external HR system via code retired webhooks. You will register an outbound webhook that triggers on routing.wrappingskills.updated. The service also tracks archiving latency, retire success rates, and generates archiving audit logs for routing governance.
import com.mulesoft.weave.sdk.platform.client.api.WebhookApi;
import com.mulesoft.weave.sdk.platform.client.model.Webhook;
import com.mulesoft.weave.sdk.platform.client.model.WebhookPost;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ArchiverGovernanceManager {
private static final Logger logger = LoggerFactory.getLogger(ArchiverGovernanceManager.class);
private final WebhookApi webhookApi;
private final Map<String, Long> latencyTracker = new ConcurrentHashMap<>();
private final Map<String, Boolean> successTracker = new ConcurrentHashMap<>();
public ArchiverGovernanceManager(PureCloudPlatformClientV2 platformClient) {
this.webhookApi = new WebhookApi(platformClient);
}
public void registerRetireWebhook(String externalHrEndpoint) throws ApiException {
WebhookPost webhookConfig = new WebhookPost();
webhookConfig.setLabel("WrapupSkill Retire Sync");
webhookConfig.setDescription("Synchronizes archiving events with external HR system");
webhookConfig.setUrl(externalHrEndpoint);
webhookConfig.setEvents(List.of("routing.wrappingskills.updated"));
webhookConfig.setTargetUrl(externalHrEndpoint);
webhookConfig.setIsActive(true);
// Format verification for webhook payload structure
webhookConfig.setContentType("application/json");
Webhook registeredWebhook = webhookApi.postPlatformWebhook(webhookConfig);
logger.info("Registered retire webhook with ID: {}", registeredWebhook.getId());
}
public void recordArchivingMetrics(String wrapupSkillId, Instant startTime, boolean success) {
long latencyMs = Instant.now().toEpochMilli() - startTime.toEpochMilli();
latencyTracker.put(wrapupSkillId, latencyMs);
successTracker.put(wrapupSkillId, success);
// Generate archiving audit logs for routing governance
logger.info("AUDIT | WrapupSkill={} | Action=RETIRED | Latency={}ms | Success={} | Timestamp={}",
wrapupSkillId, latencyMs, success, Instant.now());
}
public Map<String, Object> getArchiveEfficiencyReport() {
long totalAttempts = successTracker.size();
long successfulRetires = successTracker.values().stream().filter(v -> v).count();
double successRate = totalAttempts > 0 ? (double) successfulRetires / totalAttempts * 100 : 0;
double avgLatency = latencyTracker.values().stream().mapToLong(Long::longValue).average().orElse(0);
return Map.of(
"totalAttempts", totalAttempts,
"successfulRetires", successfulRetires,
"retireSuccessRate", successRate,
"averageLatencyMs", avgLatency
);
}
}
Required OAuth Scope: platform:webhook:write
Webhook Payload Example (sent to external HR system):
{
"event": "routing.wrappingskills.updated",
"timestamp": "2024-05-15T10:30:00Z",
"data": {
"id": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
"code": "CUSTOMER_REQUESTED_CANCEL",
"enabled": false,
"version": 4
}
}
Complete Working Example
The following class orchestrates the entire archiving workflow. It initializes the SDK, validates the schema, checks dependencies, executes the atomic PATCH, registers the webhook, and records governance metrics.
import com.mulesoft.weave.sdk.platform.client.PureCloudPlatformClientV2;
import com.mulesoft.weave.sdk.platform.client.ApiException;
import com.mulesoft.weave.sdk.platform.client.auth.OAuthClient;
import com.mulesoft.weave.sdk.platform.client.auth.OAuthResponse;
import com.mulesoft.weave.sdk.platform.client.api.RoutingApi;
import com.mulesoft.weave.sdk.platform.client.api.AnalyticsApi;
import com.mulesoft.weave.sdk.platform.client.api.WebhookApi;
import com.mulesoft.weave.sdk.platform.client.model.WrapupSkill;
import com.mulesoft.weave.sdk.platform.client.model.WrapupSkillPatch;
import com.mulesoft.weave.sdk.platform.client.model.ConversationSummaryQuery;
import com.mulesoft.weave.sdk.platform.client.model.ConversationSummaryQueryResponse;
import com.mulesoft.weave.sdk.platform.client.model.WebhookPost;
import com.mulesoft.weave.sdk.platform.client.model.Webhook;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WrapupSkillArchiver {
private static final Logger logger = LoggerFactory.getLogger(WrapupSkillArchiver.class);
private static final String BASE_URL = "https://api.mypurecloud.com";
private static final String CLIENT_ID = "YOUR_CLIENT_ID";
private static final String CLIENT_SECRET = "YOUR_CLIENT_SECRET";
private static final List<String> SCOPES = List.of(
"routing:wrapupskill:read", "routing:wrapupskill:write",
"analytics:conversations:view", "platform:webhook:write"
);
private static final int RETENTION_WINDOW_DAYS = 90;
private static final int MAX_RETRIES = 3;
private static final long BASE_RETRY_DELAY_MS = 1000;
private final RoutingApi routingApi;
private final AnalyticsApi analyticsApi;
private final WebhookApi webhookApi;
private final Map<String, Long> latencyTracker = new ConcurrentHashMap<>();
private final Map<String, Boolean> successTracker = new ConcurrentHashMap<>();
public WrapupSkillArchiver() throws ApiException {
var platformClient = PureCloudPlatformClientV2.create(BASE_URL);
var oauthResponse = platformClient.getOAuthClient().clientCredentials(CLIENT_ID, CLIENT_SECRET, SCOPES);
if (oauthResponse.getAccessToken() == null) {
throw new RuntimeException("OAuth initialization failed");
}
this.routingApi = new RoutingApi(platformClient);
this.analyticsApi = new AnalyticsApi(platformClient);
this.webhookApi = new WebhookApi(platformClient);
}
public void archiveWrapupSkill(String wrapupSkillId, String expectedCode, String expectedCategory, String externalHrEndpoint) {
Instant startTime = Instant.now();
try {
// Step 1: Validate schema, code-ref, and category-matrix
WrapupSkill skill = routingApi.getRoutingWrapupskill(wrapupSkillId, null, null);
if (!skill.getCode().equals(expectedCode) || !skill.getCategory().equalsIgnoreCase(expectedCategory)) {
throw new IllegalArgumentException("Schema validation failed: code or category mismatch");
}
// Step 2: Usage-stat calculation and dependency-check evaluation
Instant endTime = Instant.now();
Instant windowStart = endTime.minus(RETENTION_WINDOW_DAYS, ChronoUnit.DAYS);
ConversationSummaryQuery query = new ConversationSummaryQuery();
query.setInterval(String.format("%s/%s", windowStart.toString(), endTime.toString()));
query.setFilter("wrapupSkillId", wrapupSkillId);
ConversationSummaryQueryResponse analyticsResponse = analyticsApi.postAnalyticsConversationsSummaryQuery(query);
if (analyticsResponse.getTotalCount() != null && analyticsResponse.getTotalCount() > 0) {
throw new IllegalStateException("Active dependency detected. Cannot retire within retention-constraints.");
}
// Step 3: Atomic HTTP PATCH with retire directive and automatic disable trigger
WrapupSkillPatch patch = new WrapupSkillPatch();
patch.setOp("replace");
patch.setPath("/enabled");
patch.setValue(false);
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
routingApi.patchRoutingWrapupskill(wrapupSkillId, skill.getVersion(), patch, null, null);
break;
} catch (ApiException ex) {
if (ex.getCode() == 429 && attempt < MAX_RETRIES) {
Thread.sleep(BASE_RETRY_DELAY_MS * (long) Math.pow(2, attempt - 1));
} else {
throw ex;
}
}
}
// Step 4: Webhook synchronization and audit logging
WebhookPost webhook = new WebhookPost();
webhook.setLabel("Wrapup Retire Sync");
webhook.setUrl(externalHrEndpoint);
webhook.setEvents(List.of("routing.wrappingskills.updated"));
webhook.setTargetUrl(externalHrEndpoint);
webhook.setIsActive(true);
webhook.setContentType("application/json");
webhookApi.postPlatformWebhook(webhook);
recordMetrics(wrapupSkillId, startTime, true);
logger.info("Successfully archived wrap-up skill: {}", wrapupSkillId);
} catch (Exception e) {
recordMetrics(wrapupSkillId, startTime, false);
logger.error("Archiving failed for {}: {}", wrapupSkillId, e.getMessage());
throw new RuntimeException("Archiving pipeline aborted", e);
}
}
private void recordMetrics(String id, Instant start, boolean success) {
long latency = Instant.now().toEpochMilli() - start.toEpochMilli();
latencyTracker.put(id, latency);
successTracker.put(id, success);
logger.info("AUDIT | Skill={} | Action=ARCHIVE | Latency={}ms | Success={} | TS={}",
id, latency, success, Instant.now());
}
public Map<String, Object> getEfficiencyReport() {
long total = successTracker.size();
long success = successTracker.values().stream().filter(v -> v).count();
double rate = total > 0 ? (double) success / total * 100 : 0;
double avgLatency = latencyTracker.values().stream().mapToLong(Long::longValue).average().orElse(0);
return Map.of("totalAttempts", total, "successRate", rate, "avgLatencyMs", avgLatency);
}
public static void main(String[] args) {
try {
var archiver = new WrapupSkillArchiver();
archiver.archiveWrapupSkill(
"your-wrapup-skill-id",
"CUSTOMER_REQUESTED_CANCEL",
"Customer Requested",
"https://hr-system.example.com/api/v1/webhooks/genesys-retire"
);
System.out.println(archiver.getEfficiencyReport());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Common Errors & Debugging
Error: HTTP 400 Bad Request
- Cause: The
code-reforcategory-matrixdoes not match the existing wrap-up skill, or the PATCH payload violates Genesys Cloud schema validation. - Fix: Verify the
codeandcategoryfields before constructing the request. Ensure theversionparameter matches the latest retrieved version. - Code Fix: Add explicit string comparison before PATCH execution. Validate payload structure against
WrapupSkillPatchmodel.
Error: HTTP 403 Forbidden
- Cause: Missing OAuth scopes or insufficient organizational permissions.
- Fix: Ensure the client credentials include
routing:wrapupskill:writeandanalytics:conversations:view. Verify the user associated with the credentials has theRouting AdministratororAnalytics Viewerrole. - Code Fix: Log the
oauthResponse.getScopes()after token acquisition to verify scope propagation.
Error: HTTP 409 Conflict
- Cause:
active-task checkingfailed. The wrap-up skill is referenced in a queue configuration, skill group, or has open conversations. - Fix: Resolve routing dependencies first. Remove the skill from queue
assignmentsordefaultWrapupCodebefore retiring. Wait for active interactions to close. - Code Fix: Implement a pre-flight check against
/api/v2/routing/queuesto identify dependent queues.
Error: HTTP 429 Too Many Requests
- Cause: Rate-limit cascade triggered by rapid successive PATCH or Analytics queries.
- Fix: The example implements exponential backoff. Adjust
BASE_RETRY_DELAY_MSorMAX_RETRIESfor high-volume archiving batches. - Code Fix: Monitor
Retry-Afterheaders in theApiExceptionresponse and dynamically adjust sleep duration.