Updating Genesys Cloud Outbound DNC Lists in Real Time with Batching and Deduplication in Java
What You Will Build
A Java application that ingests a stream of phone numbers, deduplicates them using a HashSet, and applies batched PATCH requests to update expiration dates on existing DNC list entries. This tutorial uses the Genesys Cloud CX Outbound DNC REST API and the official Java SDK. The implementation runs in Java 17 with Maven dependencies.
Prerequisites
- OAuth client credentials flow with
outbound:dnc:readandoutbound:dnc:writescopes - Genesys Cloud Java SDK version 2.0.0 or higher
- Java 17 runtime
- Maven project with the following dependencies:
com.genesis:genesyscloud-java-sdk:2.0.0com.google.code.gson:gson:2.10.1org.slf4j:slf4j-api:2.0.9ch.qos.logback:logback-classic:1.4.11
Authentication Setup
The Genesys Cloud Java SDK handles token acquisition and automatic refresh when configured with client credentials. You must initialize the PureCloudPlatformClientV2 instance before creating any API clients. The SDK caches the access token in memory and refreshes it transparently before expiration.
import com.genesis.platform.client.PureCloudPlatformClientV2;
import com.genesis.platform.client.api.DncApi;
import com.genesis.platform.client.auth.ClientCredentialFlow;
import com.genesis.platform.client.auth.OAuth2Client;
public class DncAuthSetup {
public static PureCloudPlatformClientV2 initializeClient(String envUrl, String clientId, String clientSecret) {
PureCloudPlatformClientV2 client = new PureCloudPlatformClientV2();
OAuth2Client oAuth = new OAuth2Client(client);
// Configure client credentials flow
ClientCredentialFlow flow = new ClientCredentialFlow.Builder(clientId, clientSecret)
.setEnvironment(envUrl)
.build();
// Authenticate and cache token
oAuth.authenticate(flow);
return client;
}
}
The authentication flow requires the outbound:dnc:read scope to fetch existing entries and outbound:dnc:write to modify them. If you omit either scope, the SDK throws an ApiException with HTTP 403.
Implementation
Step 1: Fetch Existing DNC Entries and Build a Lookup Map
Before updating entries, you must retrieve the current state of the DNC list. The API returns paginated results. You must iterate through all pages to build a complete map of dncEntryId to phoneNumber. This map enables fast deduplication and provides the IDs required for PATCH requests.
import com.genesis.platform.client.api.DncApi;
import com.genesis.platform.client.model.DncEntryEntityListing;
import com.genesis.platform.client.model.DncEntry;
import java.util.HashMap;
import java.util.Map;
public DncEntry fetchExistingEntries(DncApi dncApi, String dncListId) throws Exception {
Map<String, String> entryIdToPhone = new HashMap<>();
Map<String, DncEntry> entryIdToObject = new HashMap<>();
String continuationToken = null;
int pageSize = 100;
do {
DncEntryEntityListing listing = dncApi.dncListsEntriesGet(
dncListId,
pageSize,
continuationToken,
null, // sortOrder
null, // expand
null // locationId
);
if (listing.getEntities() != null) {
for (DncEntry entry : listing.getEntities()) {
entryIdToPhone.put(entry.getId(), entry.getPhoneNumber());
entryIdToObject.put(entry.getId(), entry);
}
}
continuationToken = listing.getContinuationToken();
} while (continuationToken != null);
return entryIdToObject;
}
The dncListsEntriesGet method maps to GET /api/v2/outbound/dnc/lists/{dncListId}/entries. The continuationToken parameter handles pagination. If the list contains fewer than 100 entries, the token returns null and the loop terminates.
Step 2: Deduplicate Phone Numbers Using a HashSet
Real-time ingestion sources often send duplicate phone numbers. You must filter duplicates against both the incoming batch and the existing DNC entries. A HashSet provides O(1) lookup time, which prevents unnecessary API calls and reduces payload size.
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public List<Map.Entry<String, DncEntry>> prepareUpdates(
List<String> incomingPhones,
Map<String, DncEntry> existingEntries) {
// Map phone number to entry ID for reverse lookup
Map<String, String> phoneToEntryId = existingEntries.values().stream()
.collect(Collectors.toMap(DncEntry::getPhoneNumber, DncEntry::getId, (v1, v2) -> v1));
// HashSet for deduplication
HashSet<String> processedPhones = new HashSet<>();
List<Map.Entry<String, DncEntry>> updatesToApply = new java.util.ArrayList<>();
for (String phone : incomingPhones) {
if (!processedPhones.add(phone)) {
// Duplicate in incoming batch, skip
continue;
}
String entryId = phoneToEntryId.get(phone);
if (entryId != null) {
DncEntry existing = existingEntries.get(entryId);
updatesToApply.add(Map.entry(entryId, existing));
}
// Phone not in list: handled separately via POST, outside this PATCH scope
}
return updatesToApply;
}
The HashSet.add() method returns false when the element already exists. This single line handles intra-batch deduplication. The reverse lookup map ensures you only target entries that actually exist in the DNC list.
Step 3: Construct PATCH Payloads and Batch Requests
The DNC entry PATCH endpoint requires a DncEntryPatch object. You must specify only the fields you intend to modify. The API ignores null fields. You will batch updates into groups of 20 to stay within rate limits while maintaining throughput.
import com.genesis.platform.client.model.DncEntryPatch;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
public List<DncEntryPatch> buildPatchPayloads(List<Map.Entry<String, DncEntry>> updates, OffsetDateTime newExpiration) {
return updates.stream().map(entry -> {
DncEntryPatch patch = new DncEntryPatch();
patch.setExpirationDate(newExpiration);
patch.setStatus("active");
return patch;
}).collect(Collectors.toList());
}
The DncEntryPatch constructor does not require an ID. The ID is passed as a path parameter in the API call. Setting status to active ensures the entry remains enforced. The expirationDate field uses OffsetDateTime to match the ISO 8601 format expected by the API.
Step 4: Execute Batched PATCH Requests with TPS Throttling
Genesys Cloud enforces a default transaction limit of 100 requests per second for most endpoints, but DNC operations recommend a conservative limit of 10 to 15 TPS to prevent cascading 429 errors. You will use a ScheduledExecutorService to release execution permits at a fixed rate, ensuring strict TPS compliance.
import com.genesis.platform.client.ApiException;
import com.genesis.platform.client.api.DncApi;
import com.genesis.platform.client.model.DncEntryPatch;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public void executeBatchedPatches(
DncApi dncApi,
List<String> entryIds,
List<DncEntryPatch> payloads,
int tpsLimit) throws Exception {
int batchSize = 20;
ExecutorService executor = Executors.newFixedThreadPool(batchSize);
Semaphore semaphore = new Semaphore(0);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger retryCount = new AtomicInteger(0);
// Release permits at TPS rate
ScheduledExecutorService rateLimiter = Executors.newSingleThreadScheduledExecutor();
rateLimiter.scheduleAtFixedRate(() -> semaphore.release(tpsLimit), 0, 1, TimeUnit.SECONDS);
try {
for (int i = 0; i < entryIds.size(); i += batchSize) {
int end = Math.min(i + batchSize, entryIds.size());
List<String> batchIds = entryIds.subList(i, end);
List<DncEntryPatch> batchPayloads = payloads.subList(i, end);
List<Future<?>> futures = new java.util.ArrayList<>();
for (int j = 0; j < batchIds.size(); j++) {
final String id = batchIds.get(j);
final DncEntryPatch payload = batchPayloads.get(j);
futures.add(executor.submit(() -> {
try {
semaphore.acquire(); // Blocks until TPS permit available
dncApi.dncEntriesPatch(id, payload);
successCount.incrementAndGet();
} catch (ApiException e) {
if (e.getCode() == 429) {
retryCount.incrementAndGet();
Thread.sleep(500); // Backoff on rate limit
dncApi.dncEntriesPatch(id, payload); // Retry once
successCount.incrementAndGet();
} else {
throw e;
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}));
}
// Wait for current batch to complete before starting next
for (Future<?> f : futures) {
f.get();
}
}
} finally {
rateLimiter.shutdown();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
}
}
The Semaphore starts at zero. The scheduled executor releases exactly tpsLimit permits every second. Each worker thread blocks on semaphore.acquire() until a permit is available. This guarantees you never exceed the TPS threshold. The 429 retry logic includes a 500 millisecond exponential backoff and a single retry attempt.
HTTP Request/Response Cycle for PATCH
PATCH /api/v2/outbound/dnc/entries/00000000-0000-0000-0000-000000000000 HTTP/1.1
Host: api.mypurecloud.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...
Content-Type: application/json
Accept: application/json
{
"expirationDate": "2025-12-31T23:59:59.000Z",
"status": "active"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "00000000-0000-0000-0000-000000000000",
"phoneNumber": "+15551234567",
"listId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "active",
"effectiveDate": "2024-01-01T00:00:00.000Z",
"expirationDate": "2025-12-31T23:59:59.000Z",
"selfUri": "/api/v2/outbound/dnc/entries/00000000-0000-0000-0000-000000000000"
}
Complete Working Example
import com.genesis.platform.client.PureCloudPlatformClientV2;
import com.genesis.platform.client.api.DncApi;
import com.genesis.platform.client.auth.ClientCredentialFlow;
import com.genesis.platform.client.auth.OAuth2Client;
import com.genesis.platform.client.model.DncEntry;
import com.genesis.platform.client.model.DncEntryEntityListing;
import com.genesis.platform.client.model.DncEntryPatch;
import com.genesis.platform.client.ApiException;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class DncRealTimeUpdater {
private final PureCloudPlatformClientV2 client;
private final DncApi dncApi;
private final String dncListId;
public DncRealTimeUpdater(String envUrl, String clientId, String clientSecret, String dncListId) {
this.client = new PureCloudPlatformClientV2();
OAuth2Client oAuth = new OAuth2Client(this.client);
ClientCredentialFlow flow = new ClientCredentialFlow.Builder(clientId, clientSecret)
.setEnvironment(envUrl)
.build();
oAuth.authenticate(flow);
this.dncApi = this.client.getDncApi();
this.dncListId = dncListId;
}
public void updateDncEntries(List<String> incomingPhones, OffsetDateTime newExpiration, int tpsLimit) throws Exception {
Map<String, DncEntry> existingEntries = fetchExistingEntries();
List<Map.Entry<String, DncEntry>> updates = prepareUpdates(incomingPhones, existingEntries);
if (updates.isEmpty()) {
System.out.println("No updates required. All phones are duplicates or not in list.");
return;
}
List<String> entryIds = updates.stream().map(Map.Entry::getKey).collect(Collectors.toList());
List<DncEntryPatch> payloads = buildPatchPayloads(updates, newExpiration);
executeBatchedPatches(entryIds, payloads, tpsLimit);
}
private Map<String, DncEntry> fetchExistingEntries() throws Exception {
Map<String, DncEntry> entryMap = new HashMap<>();
String continuationToken = null;
do {
DncEntryEntityListing listing = dncApi.dncListsEntriesGet(
dncListId, 100, continuationToken, null, null, null);
if (listing.getEntities() != null) {
for (DncEntry entry : listing.getEntities()) {
entryMap.put(entry.getId(), entry);
}
}
continuationToken = listing.getContinuationToken();
} while (continuationToken != null);
return entryMap;
}
private List<Map.Entry<String, DncEntry>> prepareUpdates(
List<String> incomingPhones,
Map<String, DncEntry> existingEntries) {
Map<String, String> phoneToEntryId = existingEntries.values().stream()
.collect(Collectors.toMap(DncEntry::getPhoneNumber, DncEntry::getId, (v1, v2) -> v1));
HashSet<String> processedPhones = new HashSet<>();
List<Map.Entry<String, DncEntry>> updates = new ArrayList<>();
for (String phone : incomingPhones) {
if (!processedPhones.add(phone)) continue;
String entryId = phoneToEntryId.get(phone);
if (entryId != null) {
updates.add(Map.entry(entryId, existingEntries.get(entryId)));
}
}
return updates;
}
private List<DncEntryPatch> buildPatchPayloads(
List<Map.Entry<String, DncEntry>> updates,
OffsetDateTime newExpiration) {
return updates.stream().map(entry -> {
DncEntryPatch patch = new DncEntryPatch();
patch.setExpirationDate(newExpiration);
patch.setStatus("active");
return patch;
}).collect(Collectors.toList());
}
private void executeBatchedPatches(
List<String> entryIds,
List<DncEntryPatch> payloads,
int tpsLimit) throws Exception {
int batchSize = 20;
ExecutorService executor = Executors.newFixedThreadPool(batchSize);
Semaphore semaphore = new Semaphore(0);
ScheduledExecutorService rateLimiter = Executors.newSingleThreadScheduledExecutor();
rateLimiter.scheduleAtFixedRate(() -> semaphore.release(tpsLimit), 0, 1, TimeUnit.SECONDS);
try {
for (int i = 0; i < entryIds.size(); i += batchSize) {
int end = Math.min(i + batchSize, entryIds.size());
List<String> batchIds = entryIds.subList(i, end);
List<DncEntryPatch> batchPayloads = payloads.subList(i, end);
List<Future<?>> futures = new ArrayList<>();
for (int j = 0; j < batchIds.size(); j++) {
final String id = batchIds.get(j);
final DncEntryPatch payload = batchPayloads.get(j);
futures.add(executor.submit(() -> {
try {
semaphore.acquire();
dncApi.dncEntriesPatch(id, payload);
} catch (ApiException e) {
if (e.getCode() == 429) {
Thread.sleep(500);
dncApi.dncEntriesPatch(id, payload);
} else {
throw e;
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}));
}
for (Future<?> f : futures) {
f.get();
}
}
} finally {
rateLimiter.shutdown();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
}
}
public static void main(String[] args) {
try {
String envUrl = "https://api.mypurecloud.com";
String clientId = "YOUR_CLIENT_ID";
String clientSecret = "YOUR_CLIENT_SECRET";
String dncListId = "YOUR_DNC_LIST_ID";
DncRealTimeUpdater updater = new DncRealTimeUpdater(envUrl, clientId, clientSecret, dncListId);
List<String> newPhones = Arrays.asList("+15551234567", "+15559876543", "+15551234567");
OffsetDateTime expiration = OffsetDateTime.now().plusDays(365);
updater.updateDncEntries(newPhones, expiration, 10);
System.out.println("DNC list update completed successfully.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Common Errors & Debugging
Error: HTTP 401 Unauthorized
- Cause: The OAuth token expired or the client credentials are invalid. The SDK may have failed to refresh the token.
- Fix: Verify the
clientIdandclientSecretmatch a registered OAuth 2.0 client in the Genesys Cloud admin console. Ensure the client type is set toconfidential. Restart the application to force a fresh token acquisition. - Code Fix: Wrap the initialization in a try-catch block and log the exception message. The SDK throws
ApiExceptionwith code 401 when authentication fails.
Error: HTTP 403 Forbidden
- Cause: The OAuth client lacks the
outbound:dnc:writeoroutbound:dnc:readscope. - Fix: Navigate to the Genesys Cloud admin console, locate your OAuth client, and add both DNC scopes. The changes apply immediately without requiring a token refresh.
- Code Fix: Validate scopes during initialization by calling
oAuth.getAccessToken().getScopes()and asserting both required scopes are present.
Error: HTTP 429 Too Many Requests
- Cause: The application exceeded the Genesys Cloud TPS limit for DNC endpoints. This occurs when batch sizes are too large or the rate limiter is misconfigured.
- Fix: Reduce the
tpsLimitparameter to 5 or lower. Increase the backoff delay in the retry logic. Monitor theRetry-Afterheader in the 429 response body for precise wait times. - Code Fix: The provided implementation already includes a 500 millisecond backoff and a single retry. For production workloads, implement exponential backoff by multiplying the delay on subsequent retries.
Error: HTTP 404 Not Found
- Cause: The
dncListIdis incorrect or the DNC entry ID does not exist. - Fix: Verify the list ID using
GET /api/v2/outbound/dnc/lists. Ensure the phone numbers in your input list actually exist in the target DNC list. The deduplication step filters out missing numbers, but if the input contains invalid IDs, the API returns 404. - Code Fix: Add a validation step that checks
entryId != nullbefore constructing the PATCH payload. The provided code already performs this check.