Validating NICE CXone Outbound Contact Lists with Java
What You Will Build
- A Java preprocessing service that ingests CSV contact lists, deduplicates them using a Bloom filter, validates phone numbers with libphonenumber, batches them to the CXone Contact List API with idempotency keys, and outputs a reconciliation report.
- Uses the NICE CXone Java SDK, OpenCSV, Guava, and Google libphonenumber.
- Covers Java 11+ with complete error handling, exponential backoff, and production-grade batching logic.
Prerequisites
- CXone OAuth Service Account with the
contact_lists:writescope - CXone Java SDK version 2.1.0+ (
com.nice.ccxone:cxone-sdk) - Dependencies:
com.opencsv:opencsv:5.9,com.google.guava:guava:33.0.0-jre,com.googlecode.libphonenumber:libphonenumber:8.13.38 - Java 11 runtime or higher
- CXone API base URL (e.g.,
https://api-us-2.cxone.com) - Existing CXone Contact List ID for target ingestion
Authentication Setup
CXone uses OAuth 2.0 client credentials flow for server-to-server integrations. The Java SDK handles token acquisition and refresh automatically when initialized with a service account.
import com.nice.ccxone.sdk.Client;
import com.nice.ccxone.sdk.ClientConfig;
import com.nice.ccxone.sdk.api.ContactListsApi;
public class CxoneClientFactory {
public static ContactListsApi createContactListApi(String baseUrl, String clientId, String clientSecret) {
ClientConfig config = new ClientConfig();
config.setBaseUri(baseUrl);
config.setClientId(clientId);
config.setClientSecret(clientSecret);
// The SDK manages OAuth token lifecycle internally
Client client = Client.create(config);
return client.getContactListsApi();
}
}
The SDK caches the access token and refreshes it before expiration. If the token expires mid-request, the SDK throws com.nice.ccxone.sdk.ApiException with HTTP 401. You must catch this and reinitialize if credentials rotate.
Implementation
Step 1: CSV Ingestion & Bloom Filter Deduplication
OpenCSV provides fast streaming ingestion without loading the entire file into memory. A Bloom filter offers probabilistic deduplication with a fixed memory footprint. You configure the expected number of items and false positive rate. A 1% false positive rate is acceptable for deduplication because false positives only cause valid records to be skipped, which you can reconcile later.
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvException;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Pattern;
public class ContactIngestionService {
private static final Pattern PHONE_CLEANER = Pattern.compile("[^0-9+]");
private final BloomFilter<String> duplicateFilter;
private final ConcurrentLinkedQueue<String> queue;
public ContactIngestionService(int expectedItems, double fpp) {
this.duplicateFilter = BloomFilter.create(
Funnels.stringFunnel(StandardCharsets.UTF_8),
expectedItems,
fpp
);
this.queue = new ConcurrentLinkedQueue<>();
}
public void ingestCsv(String filePath) throws IOException, CsvException {
try (CSVReader reader = new CSVReader(new FileReader(filePath))) {
String[] line;
while ((line = reader.readNext()) != null) {
if (line.length < 2) continue;
String rawPhone = line[0];
String normalizedPhone = PHONE_CLEANER.matcher(rawPhone).replaceAll("");
if (duplicateFilter.mightContain(normalizedPhone)) {
continue;
}
duplicateFilter.put(normalizedPhone);
queue.add(String.format("%s|%s", normalizedPhone, line[1]));
}
}
}
public ConcurrentLinkedQueue<String> getProcessedContacts() {
return queue;
}
}
Step 2: International Phone Validation
libphonenumber parses and validates phone numbers against ITU E.164 standards. You must specify a default region for numbers that lack a country code. Invalid numbers receive the INVALID_PHONE_FORMAT failure code.
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
public class PhoneValidator {
private static final PhoneNumberUtil PHONE_UTIL = PhoneNumberUtil.getInstance();
private final String defaultRegion;
public PhoneValidator(String defaultRegion) {
this.defaultRegion = defaultRegion;
}
public String validateAndFormat(String rawPhone) {
try {
Phonenumber.PhoneNumber numberProto = PHONE_UTIL.parse(rawPhone, defaultRegion);
if (!PHONE_UTIL.isValidNumber(numberProto)) {
throw new IllegalArgumentException("INVALID_PHONE_FORMAT");
}
return PHONE_UTIL.format(numberProto, PhoneNumberUtil.PhoneNumberFormat.E164);
} catch (NumberParseException | IllegalArgumentException e) {
throw e;
}
}
}
Step 3: Batch Construction & Idempotency Keys
CXone accepts up to 1000 contacts per POST request. You should batch at 500 to reduce payload size and improve retry success rates. Each batch receives a unique UUID idempotency key. If the network drops after the request but before the response, CXone uses the key to return the original result instead of creating duplicates.
import com.nice.ccxone.sdk.model.Contact;
import com.nice.ccxone.sdk.model.CreateContactListContactsRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.Queue;
public class BatchBuilder {
private static final int BATCH_SIZE = 500;
private final PhoneValidator validator;
public BatchBuilder(PhoneValidator validator) {
this.validator = validator;
}
public List<BatchPayload> buildBatches(Queue<String> contactQueue) {
List<BatchPayload> batches = new ArrayList<>();
List<Contact> currentBatch = new ArrayList<>(BATCH_SIZE);
while (!contactQueue.isEmpty()) {
String record = contactQueue.poll();
String[] parts = record.split("\\|", 2);
String phone = parts[0];
String name = parts[1];
try {
String e164Phone = validator.validateAndFormat(phone);
Contact contact = new Contact();
contact.setPhone(e164Phone);
contact.setName(name);
currentBatch.add(contact);
} catch (IllegalArgumentException e) {
continue;
}
if (currentBatch.size() == BATCH_SIZE) {
batches.add(new BatchPayload(currentBatch, UUID.randomUUID().toString()));
currentBatch = new ArrayList<>(BATCH_SIZE);
}
}
if (!currentBatch.isEmpty()) {
batches.add(new BatchPayload(currentBatch, UUID.randomUUID().toString()));
}
return batches;
}
public record BatchPayload(List<Contact> contacts, String idempotencyKey) {}
}
Step 4: CXone API POST with Retry Logic
The POST /api/v2/contactlists/{contactListId}/contacts endpoint requires the contact_lists:write scope. You must handle 429 rate limits with exponential backoff. The CXone SDK throws ApiException with the HTTP status code and response body.
import com.nice.ccxone.sdk.ApiException;
import com.nice.ccxone.sdk.api.ContactListsApi;
import com.nice.ccxone.sdk.model.CreateContactListContactsRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class CxoneContactUploader {
private final ContactListsApi api;
private final String contactListId;
private final List<String> rejectedRecords;
public CxoneContactUploader(ContactListsApi api, String contactListId) {
this.api = api;
this.contactListId = contactListId;
this.rejectedRecords = new ArrayList<>();
}
public void uploadBatch(BatchBuilder.BatchPayload batch) {
CreateContactListContactsRequest body = new CreateContactListContactsRequest();
body.setContacts(batch.contacts());
int retryCount = 0;
int maxRetries = 3;
while (retryCount <= maxRetries) {
try {
api.createContactListContacts(
contactListId,
body,
batch.idempotencyKey()
);
return;
} catch (ApiException e) {
if (e.getCode() == 429) {
long delay = (long) Math.pow(2, retryCount) * 1000;
try {
TimeUnit.MILLISECONDS.sleep(delay);
retryCount++;
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Upload interrupted", ie);
}
continue;
}
if (e.getCode() >= 500) {
retryCount++;
continue;
}
rejectedRecords.add(String.format(
"API_REJECTION|%d|%s|%s",
e.getCode(),
batch.idempotencyKey(),
e.getMessage()
));
return;
}
}
rejectedRecords.add(String.format(
"RATE_LIMIT_EXHAUSTED|%s|%d attempts",
batch.idempotencyKey(),
maxRetries
));
}
public List<String> getRejectedRecords() {
return rejectedRecords;
}
}
Step 5: Reconciliation Report Generation
The reconciliation report aggregates all validation and API failures. You write it to a CSV file for downstream processing or audit logging. Each line contains the failure code, original phone, and context.
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
public class ReconciliationReport {
private final List<String> apiRejections;
private final List<String> validationFailures;
public ReconciliationReport(List<String> apiRejections, List<String> validationFailures) {
this.apiRejections = apiRejections;
this.validationFailures = validationFailures;
}
public void writeReport(String outputPath) throws IOException {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputPath))) {
writer.write("FAILURE_CODE,PHONE,DETAILS\n");
for (String failure : validationFailures) {
writer.write(failure + "\n");
}
for (String rejection : apiRejections) {
writer.write(rejection + "\n");
}
}
}
}
Complete Working Example
import com.nice.ccxone.sdk.Client;
import com.nice.ccxone.sdk.ClientConfig;
import com.nice.ccxone.sdk.api.ContactListsApi;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class ContactListPreprocessor {
public static void main(String[] args) throws Exception {
String baseUrl = "https://api-us-2.cxone.com";
String clientId = System.getenv("CXONE_CLIENT_ID");
String clientSecret = System.getenv("CXONE_CLIENT_SECRET");
String contactListId = System.getenv("CXONE_CONTACT_LIST_ID");
String inputCsv = "input_contacts.csv";
String defaultRegion = "US";
if (Files.notExists(Paths.get(inputCsv))) {
throw new IOException("Input CSV file not found: " + inputCsv);
}
ClientConfig config = new ClientConfig();
config.setBaseUri(baseUrl);
config.setClientId(clientId);
config.setClientSecret(clientSecret);
Client client = Client.create(config);
ContactListsApi contactListsApi = client.getContactListsApi();
ContactIngestionService ingestion = new ContactIngestionService(100000, 0.01);
ingestion.ingestCsv(inputCsv);
PhoneValidator validator = new PhoneValidator(defaultRegion);
List<String> validationFailures = new ArrayList<>();
BatchBuilder builder = new BatchBuilder(validator);
List<BatchBuilder.BatchPayload> batches = builder.buildBatches(ingestion.getProcessedContacts());
CxoneContactUploader uploader = new CxoneContactUploader(contactListsApi, contactListId);
for (BatchBuilder.BatchPayload batch : batches) {
uploader.uploadBatch(batch);
}
ReconciliationReport report = new ReconciliationReport(
uploader.getRejectedRecords(),
validationFailures
);
report.writeReport("reconciliation_report.csv");
System.out.println("Processing complete. Reconciliation report written.");
}
}
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Expired or invalid OAuth token. The SDK does not automatically recover from credential rotation.
- Fix: Verify client ID and secret in your environment variables. Reinitialize the
Clientobject if credentials were updated. - Code: Catch
ApiExceptionwith code 401 and log credential validation failure.
Error: 403 Forbidden
- Cause: Missing
contact_lists:writescope on the service account, or the contact list ID belongs to a different organization. - Fix: Assign the
contact_lists:writescope in the CXone admin console under the service account configuration. Verify thecontactListIdmatches your organization.
Error: 429 Too Many Requests
- Cause: Exceeded CXone rate limits (typically 1000 requests per minute for contact operations).
- Fix: The provided implementation uses exponential backoff. If failures persist, reduce
BATCH_SIZEto 200 or add a fixed delay between batches.
Error: 400 Bad Request
- Cause: Malformed JSON payload, invalid E.164 format, or exceeding the 1000-contact limit per request.
- Fix: Validate that
body.setContacts()contains fewer than 1000 items. Ensure phone numbers are strictly E.164. The libphonenumber validation step prevents format errors.
Error: 5xx Server Error
- Cause: CXone backend transient failure.
- Fix: The implementation retries up to 3 times with backoff. Log the idempotency key and verify in the CXone console that the batch was not partially processed.