# NICE CXone

> Send digital and voice interactions from NICE CXone to Dimension Labs with the universal API.

This brings in NICE CXone interactions, turn by turn: call transcripts and digital conversations (chat, email, messaging) that Interaction Analytics has analyzed. You run it on your side with the [universal API](/api/tracker). For digital channels without Interaction Analytics, see [Digital only](#digital-only).

## What you need

- CXone Interaction Analytics on your tenant (for voice transcripts and the analyzed digital transcripts).
- An app registered with NICE, which gives you a `client_id` and `client_secret`. NICE says registration typically takes 2 to 3 business days.
- A CXone user for the integration, with an access key ID and secret. The user's roles and views decide which interactions the API returns.
- A Dimension Labs API key for a new **Universal API** source. See [Your API key](/api/tracker#your-api-key).

## 1. Get API access in NICE CXone

NICE's own steps are in [Getting Started](https://developer.niceincontact.com/Documentation/GettingStarted).

1. Submit the Application Registration form described in Getting Started. Choose a back-end app and single tenant. NICE replies with your `client_id` and `client_secret`.
2. In CXone, open the app selector and go to **Admin > Employees > Create Employee**. Create a user for the integration, with no login authenticator.
3. Open that user, select the **Security** tab and click **Generate New Access Key**. Click **SHOW SECRET KEY** and store the secret. You can't view it again.
4. Get a token: `POST https://cxone.niceincontact.com/auth/token` with a form body of `grant_type=password`, `username` (the access key ID), `password` (the secret), `client_id` and `client_secret`. Tokens expire (`expires_in`), so request a new one before each run.
5. Find your API base URL: read `tenantId` from the `id_token`, then `GET https://cxone.niceincontact.com/.well-known/cxone-configuration?tenantId=YOUR_TENANT_ID`. The `api_endpoint` field is your base URL, for example `https://api-na1.niceincontact.com`.

Send the token as `Authorization: Bearer YOUR_TOKEN`.

## 2. Pull new interactions

List analyzed segments since your cursor, then get each segment's transcript:

```bash
curl -H "Authorization: Bearer $CXONE_TOKEN" \
  "$CXONE_API/interaction-analytics-gateway/v2/segments/analyzed?dateField=firstAnnotatedAt&beginningDate=2026-10-13T00:00:00.000Z&order=asc&pageSize=100"

curl -H "Authorization: Bearer $CXONE_TOKEN" \
  "$CXONE_API/interaction-analytics-gateway/v2/segments/SEGMENT_ID/analyzed-transcript"
```

- `dateField=firstAnnotatedAt` filters by when the segment was first analyzed, which never changes. That makes it a safe cursor. `beginningDate` and `endingDate` use `yyyy-MM-ddTHH:mm:ss.SSSZ` in UTC.
- `mediaType` narrows to one channel, for example `PhoneCall`, `Chat` or `EMail`.
- Each page has `links.next`. Request it until it is missing.
- A segment that wasn't transcribed (too short, one-sided, or the agent isn't set up for transcription) returns `404` on the transcript endpoint. Skip it.
- An interaction can have several segments when it is transferred or conferenced. `interactionId` ties them together.

## 3. Map the fields

| Dimension Labs | NICE CXone | Notes |
|---|---|---|
| `text` | `transcriptBlocks[].text` | One block per turn. |
| `userId` | segment `acdContactId` | CXone's contact id, on every block of the segment. |
| `incoming` | `transcriptBlocks[].userType` | `true` when it is `CLIENT`, `false` when `AGENT`. `channelName` carries the same value. |
| `sessionId` | `segmentId` | One segment is one conversation in Dimension Labs. |
| `conversationId` | `interactionId` | Keeps transferred segments of one interaction together. |
| `dimensionlabs_timestamp` | segment `startTime` | Blocks carry offsets, not clock times. The example adds 1,000 ms per block to `startTime` to keep the order and the one-second spacing. |
| `platformJson` | `mediaType`, `contactNo`, `resolved`, `dispositionCode`, `segmentSummaryPrimaryIntentName`, `enlightenSentimentIndexScore`, `channelType` | `acdContactId` joins to other CXone reports. |

**Personal data.** Call and chat transcripts hold names, account numbers and addresses: redact them before you send (or use CXone's own redaction, if your tenant has it). See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// nice-cxone.mjs. Environment: CXONE_API (api_endpoint from discovery), CXONE_ACCESS_KEY_ID, CXONE_ACCESS_KEY_SECRET,
// CXONE_CLIENT_ID, CXONE_CLIENT_SECRET, DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const API = process.env.CXONE_API;
const IA = `${API}/interaction-analytics-gateway/v2/segments`;
let token;

async function newToken() {
  const body = new URLSearchParams({
    grant_type: 'password', username: process.env.CXONE_ACCESS_KEY_ID, password: process.env.CXONE_ACCESS_KEY_SECRET,
    client_id: process.env.CXONE_CLIENT_ID, client_secret: process.env.CXONE_CLIENT_SECRET,
  });
  const res = await fetch('https://cxone.niceincontact.com/auth/token', { method: 'POST', body });
  if (!res.ok) throw new Error(`CXone token request returned ${res.status}: ${await res.text()}`);
  token = (await res.json()).access_token;
}

// Gets a fresh token on 401, and retries rate limits (429) and server errors (5xx). Returns null on 404.
async function get(url) {
  for (let attempt = 0; ; attempt++) {
    if (!token) await newToken();
    const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
    if (res.ok) return res.json();
    if (res.status === 404) return null; // a segment that wasn't transcribed
    if (res.status === 401 && attempt === 0) { token = null; continue; }
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`CXone returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Sends every segment first analyzed at or after `since` (Unix ms). Returns the new `since`.
export async function syncCxone({ since, historical = false }) {
  token = null; // a new token for each run
  let next = `${IA}/analyzed?dateField=firstAnnotatedAt&beginningDate=${new Date(since).toISOString()}&order=asc&pageSize=100`;
  let newest = since;
  while (next) {
    const page = await get(next);
    for (const s of page?.interactions ?? []) {
      const t = await get(`${IA}/${s.segmentId}/analyzed-transcript`);
      const start = toMillis(s.startTime);
      for (const [i, b] of (t?.transcriptBlocks ?? []).entries()) {
        if (!b.text?.trim()) continue;
        const ts = start + i * 1000;
        await sendToDimensionLabs({
          text: b.text,
          userId: String(s.acdContactId ?? s.segmentId),
          incoming: b.userType === 'CLIENT',
          sessionId: s.segmentId.replace(/:/g, '-'),
          conversationId: (s.interactionId ?? s.segmentId).replace(/:/g, '-'),
          dimensionlabs_timestamp: ts,
          platformJson: { mediaType: s.mediaType, channelType: b.channelType, acdContactId: s.acdContactId, resolved: s.resolved },
        }, { historical: historical || ts < Date.now() - HOUR });
      }
      newest = Math.max(newest, toMillis(s.firstAnnotatedAt));
    }
    next = page?.links?.next ? new URL(page.links.next, API).href : null;
  }
  return newest;
}
```

A segment exactly at the cursor comes back on the next run. That's fine: duplicates are removed automatically.

## Digital only

Without Interaction Analytics, the Digital Engagement API (on `/dfo/3.0`) gives you the messages of each digital contact (case):

- `GET /dfo/3.0/contacts` lists contacts. It filters by `status[]` (for example `closed`), `channel[]` and `routingQueueId[]`, sorts with `sorting=updatedAt` and `sortingType=desc`, and pages with `scrollToken`.
- `GET /dfo/3.0/contacts/{contactNumber}/messages` returns the messages. Each has `direction` (`inbound` from the customer, `outbound` from your side), `messageContent.payload.text`, `createdAtWithMilliseconds` and `authorEndUserIdentity`. Text is cut at 3,000 characters; when `hasAdditionalMessageContent` is `true`, get the full message with `GET /dfo/3.0/messages/{messageId}`.

NICE's documented throttling limits for Digital Engagement include 120,000 get contact requests (including get contact messages) per hour per tenant, and 5,000 contact searches per minute up to 300,000 per hour. The Digital Engagement APIs are only available on UserHub tenants.

## 5. Run it

Save the code as `nice-cxone.mjs` in a project set up as in [Set up your project](/api/tracker#set-up-your-project), and run it with the [scheduled sync](/api/scheduled-sync) runner: `import { syncCxone as sync } from './nice-cxone.mjs';`.

- **History first:** `node run.mjs 2025-01-01`, with the oldest date your tenant's data retention keeps, sends every analyzed segment since then to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends newly analyzed segments to `/track`, and they are enriched automatically every day. Analysis happens after the contact ends, so turns of a long contact, or of one analyzed late, can be more than an hour old by then: those go to `/trackhistorical`, and need a backfill.
- **Re-runs are safe:** duplicates are removed automatically.

You can also [upload a file](/data/upload-a-file) and check it first with the [file checker](/data/check-a-file), then run a backfill.

## NICE CXone reference

- [Getting Started](https://developer.niceincontact.com/Documentation/GettingStarted)
- [Interaction Analytics API](https://developer.niceincontact.com/API/InteractionAnalyticsAPI)
- [Digital Engagement API](https://developer.niceincontact.com/API/DigitalEngagementAPI)

Checked against NICE CXone's documentation on 2026-10-13.
