Bulk Updating Contact Disposition Codes and Notes Using the CXone Outbound API in Java

Bulk Updating Contact Disposition Codes and Notes Using the CXone Outbound API in Java

What You Will Build

  • Retrieve a filtered list of outbound contacts, partition them into size-limited batches, and asynchronously update their disposition codes and notes via the CXone Outbound API.
  • This tutorial uses the CXone Outbound API v2.0 bulk update endpoint and the official CXone Java SDK.
  • The implementation is written in Java 17 using java.util.concurrent for non-blocking batch execution with exponential backoff retry logic.

Prerequisites

  • CXone OAuth 2.0 Client Credentials grant with scopes: outbound:contacts:read, outbound:contacts:update
  • CXone Java SDK version 2.45.0 or later
  • Java Development Kit 17+
  • Maven dependencies:
    <dependency>
        <groupId>com.nice.cxp</groupId>
        <artifactId>cxone-api-client</artifactId>
        <version>2.45.0</version>
    </dependency>
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.10.1</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.9</version>
    </dependency>
    

Authentication Setup

CXone uses a standard OAuth 2.0 Client Credentials flow. The token endpoint varies by deployment region. The example below targets the global platform endpoint. You must cache the token and refresh it before expiration. The CXone Java SDK accepts a token string directly via ApiClient.setAccessToken().

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

public class CxoneAuthProvider {
    private static final String OAUTH_TOKEN_URL = "https://platform.niceincontact.com/oauth/token";
    private static final int TOKEN_LIFETIME_SECONDS = 3540; // 59 minutes, leaves buffer for 60-min expiry
    private final String clientId;
    private final String clientSecret;
    private String cachedToken;
    private long tokenExpiryEpoch;

    public CxoneAuthProvider(String clientId, String clientSecret) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.tokenExpiryEpoch = 0;
    }

    public synchronized String getAccessToken() throws Exception {
        if (System.currentTimeMillis() < tokenExpiryEpoch && cachedToken != null) {
            return cachedToken;
        }
        return fetchNewToken();
    }

    private String fetchNewToken() throws Exception {
        String credentials = Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes());
        String requestBody = "grant_type=client_credentials&scope=outbound:contacts:read+outbound:contacts:update";

        HttpClient client = HttpClient.newBuilder().build();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(OAUTH_TOKEN_URL))
                .header("Authorization", "Basic " + credentials)
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
                .build();

        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new RuntimeException("OAuth token request failed with status " + response.statusCode() + ": " + response.body());
        }

        JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
        cachedToken = json.get("access_token").getAsString();
        int expiresIn = json.get("expires_in").getAsInt();
        tokenExpiryEpoch = System.currentTimeMillis() + ((expiresIn - 60) * 1000L);
        return cachedToken;
    }
}

HTTP Request/Response Cycle for OAuth

POST /oauth/token HTTP/1.1
Host: platform.niceincontact.com
Authorization: Basic Y2xpZW50LWlkOmNsaWVudC1zZWNyZXQ=
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=outbound:contacts:read+outbound:contacts:update
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "outbound:contacts:read outbound:contacts:update"
}

Implementation

Step 1: Initialize CXone SDK & Base Configuration

The CXone Java SDK generates an ApiClient class that handles base path resolution, header injection, and JSON serialization. You must configure the base path to match your tenant region and attach a token provider. The SDK expects a synchronous token string, so the CxoneAuthProvider wrapper bridges the OAuth flow to the SDK configuration.

import com.nice.cxp.api.client.ApiClient;
import com.nice.cxp.api.client.auth.OAuth;

public class CxoneApiConfig {
    public static ApiClient createApiClient(String region, CxoneAuthProvider authProvider) throws Exception {
        ApiClient client = new ApiClient();
        client.setBasePath("https://" + region + ".niceincontact.com/api/outbound/v2.0");
        client.setAccessToken(authProvider.getAccessToken());
        return client;
    }
}

Step 2: Paginate Contact Retrieval & Build Batch Payloads

CXone enforces strict pagination limits. The GET /api/outbound/v2.0/contacts endpoint returns a maximum of 1000 records per page. You must iterate through pages, filter for contacts requiring disposition updates, and partition them into batches. The bulk endpoint accepts a maximum of 500 contacts per request. Exceeding this limit returns a 400 Bad Request.

import com.nice.cxp.api.client.ApiException;
import com.nice.cxp.outbound.api.ContactsApi;
import com.nice.cxp.outbound.model.Contact;
import com.nice.cxp.outbound.model.BulkUpdateContactRequest;
import com.nice.cxp.outbound.model.ContactUpdateItem;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class ContactBatchBuilder {
    private static final int PAGE_SIZE = 1000;
    private static final int BATCH_SIZE = 500;
    private static final String TARGET_CAMPAIGN_ID = "campaign-12345";
    private static final String TARGET_DISPOSITION = "BUSY";
    private static final String TARGET_NOTES = "Automated disposition update via API";

    public List<BulkUpdateContactRequest> buildBatches(ContactsApi contactsApi) throws ApiException {
        List<Contact> allContacts = new ArrayList<>();
        int pageNumber = 1;
        boolean hasMore = true;

        while (hasMore) {
            List<Contact> page = contactsApi.listContacts(
                    PAGE_SIZE, pageNumber, null, null, null, null, null, null, null, null, null, null, null, null, null
            );
            if (page == null || page.isEmpty()) {
                hasMore = false;
            } else {
                allContacts.addAll(page);
                if (page.size() < PAGE_SIZE) {
                    hasMore = false;
                } else {
                    pageNumber++;
                }
            }
        }

        List<Contact> filtered = allContacts.stream()
                .filter(c -> TARGET_CAMPAIGN_ID.equals(c.getCampaignId()))
                .filter(c -> c.getDispositionCode() == null || !c.getDispositionCode().equals(TARGET_DISPOSITION))
                .collect(Collectors.toList());

        List<BulkUpdateContactRequest> batches = new ArrayList<>();
        for (int i = 0; i < filtered.size(); i += BATCH_SIZE) {
            int end = Math.min(i + BATCH_SIZE, filtered.size());
            List<ContactUpdateItem> batchItems = new ArrayList<>();
            for (int j = i; j < end; j++) {
                Contact c = filtered.get(j);
                batchItems.add(new ContactUpdateItem()
                        .contactId(c.getContactId())
                        .dispositionCode(TARGET_DISPOSITION)
                        .notes(TARGET_NOTES)
                        .lastDispositionDate(java.time.Instant.now()));
            }
            batches.add(new BulkUpdateContactRequest().items(batchItems));
        }
        return batches;
    }
}

Expected Response for Contact Retrieval

{
  "items": [
    {
      "contactId": "contact-abc123",
      "campaignId": "campaign-12345",
      "dispositionCode": null,
      "notes": "",
      "status": "ACTIVE"
    }
  ],
  "page": 1,
  "pageSize": 1000,
  "totalCount": 2450
}

Step 3: Execute Asynchronous Bulk Updates with Retry Logic

The CXone platform enforces rate limits on bulk endpoints. You must implement concurrent execution with bounded thread pools and exponential backoff for 429 responses. The ContactsApi.bulkUpdateContacts() method maps to POST /api/outbound/v2.0/contacts/bulk. The example below uses CompletableFuture to parallelize batch submissions while tracking success and failure metrics.

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class AsyncBatchProcessor {
    private final ContactsApi contactsApi;
    private final int maxConcurrentBatches;
    private final int maxRetries;
    private final long baseDelayMs;

    public AsyncBatchProcessor(ApiClient client, int maxConcurrentBatches, int maxRetries, long baseDelayMs) {
        this.contactsApi = new ContactsApi(client);
        this.maxConcurrentBatches = maxConcurrentBatches;
        this.maxRetries = maxRetries;
        this.baseDelayMs = baseDelayMs;
    }

    public BatchExecutionResult execute(List<BulkUpdateContactRequest> batches) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(maxConcurrentBatches);
        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger failureCount = new AtomicInteger(0);

        List<CompletableFuture<Void>> futures = new ArrayList<>();
        for (BulkUpdateContactRequest batch : batches) {
            futures.add(CompletableFuture.runAsync(() -> processBatchWithRetry(batch, successCount, failureCount), executor));
        }

        CompletableFuture<Void> allDone = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
        allDone.get();
        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.MINUTES);

        return new BatchExecutionResult(successCount.get(), failureCount.get());
    }

    private void processBatchWithRetry(BulkUpdateContactRequest batch, AtomicInteger successCount, AtomicInteger failureCount) {
        int attempt = 0;
        while (attempt <= maxRetries) {
            try {
                contactsApi.bulkUpdateContacts(batch);
                successCount.incrementAndGet();
                return;
            } catch (ApiException e) {
                if (e.getCode() == 429 && attempt < maxRetries) {
                    long delay = baseDelayMs * (1L << attempt);
                    System.out.println("Rate limited on batch. Retrying in " + delay + "ms (attempt " + (attempt + 1) + ")");
                    try { Thread.sleep(delay); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); return; }
                    attempt++;
                } else {
                    System.err.println("Batch failed permanently: " + e.getMessage());
                    failureCount.incrementAndGet();
                    return;
                }
            }
        }
    }

    public record BatchExecutionResult(int successCount, int failureCount) {}
}

HTTP Request/Response Cycle for Bulk Update

POST /api/outbound/v2.0/contacts/bulk HTTP/1.1
Host: eu1.niceincontact.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
Accept: application/json

{
  "items": [
    {
      "contactId": "contact-abc123",
      "dispositionCode": "BUSY",
      "notes": "Automated disposition update via API",
      "lastDispositionDate": "2024-05-20T14:32:00Z"
    }
  ]
}
{
  "updatedCount": 1,
  "failedCount": 0,
  "errors": []
}

Complete Working Example

The following class combines authentication, pagination, batch construction, and asynchronous execution into a single runnable module. Replace the placeholder credentials and region values before execution.

import com.nice.cxp.api.client.ApiClient;
import com.nice.cxp.outbound.api.ContactsApi;
import com.nice.cxp.outbound.model.BulkUpdateContactRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.ExecutionException;

public class CxoneBulkDispositionUpdater {
    private static final Logger logger = LoggerFactory.getLogger(CxoneBulkDispositionUpdater.class);

    public static void main(String[] args) {
        String clientId = "your-client-id";
        String clientSecret = "your-client-secret";
        String region = "eu1"; // e.g., us1, eu1, ap1

        try {
            CxoneAuthProvider authProvider = new CxoneAuthProvider(clientId, clientSecret);
            ApiClient apiClient = CxoneApiConfig.createApiClient(region, authProvider);
            ContactsApi contactsApi = new ContactsApi(apiClient);

            ContactBatchBuilder builder = new ContactBatchBuilder();
            List<BulkUpdateContactRequest> batches = builder.buildBatches(contactsApi);
            logger.info("Prepared {} batch(es) for disposition update.", batches.size());

            if (batches.isEmpty()) {
                logger.info("No contacts matched the filter criteria. Exiting.");
                return;
            }

            AsyncBatchProcessor processor = new AsyncBatchProcessor(apiClient, 4, 3, 1000L);
            AsyncBatchProcessor.BatchExecutionResult result = processor.execute(batches);

            logger.info("Execution complete. Successful: {}, Failed: {}", result.successCount(), result.failureCount());
        } catch (Exception e) {
            logger.error("Fatal error during bulk update", e);
            System.exit(1);
        }
    }
}

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token has expired, the client credentials are incorrect, or the token was not attached to the SDK request.
  • Fix: Verify the ClientId and ClientSecret. Ensure the CxoneAuthProvider caches and refreshes tokens correctly. Check that client.setAccessToken() is called before any API invocation.
  • Code Fix: Add a token validation step before batch execution:
    try {
        authProvider.getAccessToken();
    } catch (Exception e) {
        throw new RuntimeException("Authentication failed. Verify client credentials.", e);
    }
    

Error: 403 Forbidden

  • Cause: The OAuth client lacks the required outbound:contacts:update scope, or the tenant policy restricts API access for the client application.
  • Fix: Navigate to the CXone Admin Console, locate the OAuth client configuration, and append outbound:contacts:update to the allowed scopes. Regenerate the token.
  • Code Fix: Explicitly request the scope during token acquisition (already included in the requestBody string).

Error: 429 Too Many Requests

  • Cause: The platform rate limit for bulk endpoints has been exceeded. CXone typically caps bulk operations at 100-200 requests per minute depending on tenant tier.
  • Fix: Reduce maxConcurrentBatches in the AsyncBatchProcessor constructor. Increase baseDelayMs for exponential backoff. Implement a token bucket or semaphore if processing thousands of batches.
  • Code Fix: The retry loop in processBatchWithRetry already handles 429 with exponential backoff. Adjust parameters:
    AsyncBatchProcessor processor = new AsyncBatchProcessor(apiClient, 2, 5, 2000L);
    

Error: 400 Bad Request

  • Cause: The batch payload exceeds the 500-item limit, contains invalid contactId formats, or references a non-existent dispositionCode in the campaign configuration.
  • Fix: Validate batch size before submission. Ensure dispositionCode matches an active disposition code defined in the CXone Outbound campaign. Verify contactId matches the format contact-<uuid>.
  • Code Fix: Add validation before adding to batchItems:
    if (c.getContactId() == null || !c.getContactId().matches("contact-[a-f0-9-]+")) {
        logger.warn("Skipping invalid contact ID: {}", c.getContactId());
        continue;
    }
    

Official References