Building a Custom CXone Real-Time Dashboard using the Reporting V2 API and WebSockets

Building a Custom CXone Real-Time Dashboard using the Reporting V2 API and WebSockets

What This Guide Covers

You are building a custom wallboard or supervisor dashboard that pulls live queue metrics, agent states, and interaction counts from NICE CXone using the Reporting V2 REST API and the Real-Time Data WebSocket feed - displayed in a browser-based frontend that updates every 5-30 seconds without page refresh. When complete, your dashboard shows current SL%, agents in each state, queue depth, and longest wait time for configurable skills - independent of the CXone native wallboard, embeddable in your operations center’s display system.


Prerequisites, Roles & Licensing

  • Licensing: CXone ACD; the Reporting V2 API is available on all ACD tiers. Real-time WebSocket feeds require CXone Real-Time Reporting entitlement - confirm with your NICE account team.
  • API Authentication: CXone OAuth 2.0 Client Credentials (for REST polling) or Access Key authentication (for WebSocket)
  • Permissions required (service account):
    • Reporting > Real-Time Adherence > View
    • Reporting > Real-Time Queue Stats > View
    • ACD > Real-Time > Queue Statistics
  • Development stack: Node.js + Express (for a backend WebSocket relay), or a pure browser-based implementation using the Fetch API and WebSocket browser object
  • External access: CXone API base URL for your region (US: api.niceincontact.com, EU: api.eu1.niceincontact.com, APAC: api.au1.niceincontact.com)

The Implementation Deep-Dive

1. Choosing Your Data Strategy: REST Polling vs. WebSocket Push

CXone offers two mechanisms for real-time metric retrieval, each with distinct trade-offs:

REST API Polling (Reporting V2)

You call GET /InContact API/services/v18.0/real-time-data/skill-data on an interval. Simple to implement; no persistent connection required. Latency = your polling interval (30-60 seconds recommended; sub-10-second polling risks rate limiting).

WebSocket Push (Real-Time Data Feed)

CXone pushes metric events to your WebSocket client as state changes occur. Near-instant updates (1-3 second latency), no polling overhead. Requires a persistent connection and reconnection logic.

Recommendation: Use WebSocket for agent state changes (visible immediately when an agent goes on break) and REST polling for aggregate skill metrics (SL%, queue depth) on a 30-second interval. This hybrid approach minimizes connection complexity while providing near-real-time agent visibility.


2. REST API Authentication and Metric Retrieval

Step 1: Obtain an OAuth token

POST https://api.niceincontact.com/InContactAuthorizationServer/Token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={YOUR_CLIENT_ID}
&client_secret={YOUR_CLIENT_SECRET}

Response:

{
  "access_token": "eyJhbGci...",
  "token_type": "bearer",
  "expires_in": 3600,
  "resource_server_base_uri": "https://api.niceincontact.com/"
}

Store resource_server_base_uri - all subsequent API calls use this base URL, not a hardcoded URL. CXone assigns tenants to specific resource servers; using the wrong base URL returns 401 even with a valid token.

Step 2: Fetch skill summary metrics

GET {resource_server_base_uri}InContact API/services/v18.0/real-time-data/skill-data?skillId=1234,5678,9012
Authorization: bearer {access_token}

Response (abbreviated):

{
  "skillData": [
    {
      "skillId": 1234,
      "skillName": "Tier 1 Voice",
      "contactsQueued": 12,
      "contactsActive": 8,
      "longestQueueDuration": 245,
      "serviceLevel": 0.82,
      "agentsAvailable": 15,
      "agentsStaffed": 20,
      "agentsOnContact": 8,
      "agentsInACW": 2,
      "currentEstimatedWaitTime": 120
    }
  ]
}

The Trap - not using resource_server_base_uri from the token response: Hardcoding api.niceincontact.com works for US tenants but fails for EU (api.eu1.niceincontact.com) and APAC tenants. Always use the resource_server_base_uri returned in the token response to construct all API URLs. Store it alongside the access token in your session state.


3. Setting Up the WebSocket Feed for Agent State Changes

The CXone real-time WebSocket feed pushes events for agent state changes as they occur.

WebSocket connection endpoint:

wss://{resource_server_base_uri_ws}/InContact API/services/v18.0/interactions/subscribe

Note: The WebSocket base URL uses wss:// (secure WebSocket) and may differ from the REST base URL - confirm the exact endpoint from NICE CXone Real-Time API documentation for your tenant.

Authentication via query parameter:

const ws = new WebSocket(
  `wss://api.niceincontact.com/InContactAPI/services/v18.0/interactions/subscribe` +
  `?access_token=${encodeURIComponent(accessToken)}`
);

Handling WebSocket events:

ws.onopen = () => {
  console.log("[WS] Connected to CXone real-time feed");
  
  // Subscribe to specific skills
  ws.send(JSON.stringify({
    "command": "subscribe",
    "skillIds": [1234, 5678, 9012]
  }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  
  switch(data.type) {
    case "agentStateChange":
      updateAgentCard(data.agentId, data.state, data.stateDuration);
      break;
    case "contactQueued":
      incrementQueueCount(data.skillId);
      break;
    case "contactDequeued":
      decrementQueueCount(data.skillId);
      break;
    case "heartbeat":
      // CXone sends periodic heartbeats - acknowledge to keep connection alive
      ws.send(JSON.stringify({ "command": "pong" }));
      break;
  }
};

ws.onerror = (error) => {
  console.error("[WS] Connection error:", error);
  scheduleReconnect();
};

ws.onclose = (event) => {
  console.warn(`[WS] Disconnected (code: ${event.code}). Reconnecting...`);
  scheduleReconnect();
};

The Trap - no reconnection logic: WebSocket connections drop. Network blips, CXone rolling restarts, token expiry - all cause disconnection. Without reconnection logic, your dashboard freezes silently. Implement exponential backoff reconnection:

let reconnectDelay = 1000; // Start at 1 second
const maxDelay = 30000; // Cap at 30 seconds

function scheduleReconnect() {
  setTimeout(() => {
    console.log(`[WS] Reconnecting (delay: ${reconnectDelay}ms)...`);
    // Re-fetch token if needed (check expiry first)
    initWebSocket();
    reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
  }, reconnectDelay);
}

// Reset delay on successful connection
ws.onopen = () => {
  reconnectDelay = 1000; // Reset on success
  // ... subscribe logic
};

4. Building the Backend Relay (Recommended for Multi-User Dashboards)

For a wallboard viewed by multiple supervisors, don’t open a WebSocket connection per browser tab - that multiplies API connections against CXone’s rate limits. Build a backend relay:

[CXone WS Feed] <--1 connection--> [Your Node.js Relay Server]
                                        |
                            [broadcasts to all connected clients]
                                        |
                    ┌───────────────────┼───────────────────┐
               [Browser 1]        [Browser 2]         [Browser 3]

Node.js relay using ws library:

const WebSocket = require("ws");
const clients = new Set();

// Server: accept browser connections
const wss = new WebSocket.Server({ port: 8080 });
wss.on("connection", (clientWs) => {
  clients.add(clientWs);
  clientWs.on("close", () => clients.delete(clientWs));
  
  // Send current state snapshot to newly connected client
  clientWs.send(JSON.stringify({ type: "snapshot", data: currentState }));
});

// CXone upstream connection
let cxoneWs;
let currentState = {};

function connectToCXone(accessToken) {
  cxoneWs = new WebSocket(`wss://api.niceincontact.com/...?access_token=${accessToken}`);
  
  cxoneWs.on("message", (rawData) => {
    const event = JSON.parse(rawData);
    
    // Update local state cache
    updateCurrentState(event);
    
    // Broadcast to all connected browsers
    const payload = JSON.stringify(event);
    clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(payload);
      }
    });
  });
}

This architecture serves thousands of browser clients with a single CXone connection.


5. Dashboard Frontend Implementation

Metric display components (HTML + vanilla JS):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CXone Live Dashboard</title>
  <style>
    body { font-family: "Inter", sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 16px; }
    .skill-card { background: #1e293b; border-radius: 12px; padding: 20px; margin-bottom: 12px; display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; }
    .metric { text-align: center; }
    .metric-value { font-size: 2rem; font-weight: 700; color: #38bdf8; }
    .metric-label { font-size: 0.75rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; }
    .sl-good { color: #34d399; }
    .sl-warning { color: #fbbf24; }
    .sl-bad { color: #f87171; }
  </style>
</head>
<body>
  <div id="dashboard"></div>

  <script>
    const ws = new WebSocket("ws://your-relay-server:8080");
    const skillData = {};

    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      
      if (msg.type === "snapshot") {
        Object.assign(skillData, msg.data);
        renderDashboard();
      } else if (msg.type === "skillDataUpdate") {
        skillData[msg.skillId] = { ...skillData[msg.skillId], ...msg.metrics };
        updateSkillCard(msg.skillId);
      }
    };

    function renderDashboard() {
      const container = document.getElementById("dashboard");
      container.innerHTML = Object.values(skillData).map(skill => renderSkillCard(skill)).join("");
    }

    function renderSkillCard(skill) {
      const slPercent = (skill.serviceLevel * 100).toFixed(1);
      const slClass = skill.serviceLevel >= 0.80 ? "sl-good" : skill.serviceLevel >= 0.70 ? "sl-warning" : "sl-bad";
      
      return `
        <div class="skill-card" id="skill-${skill.skillId}">
          <div class="metric">
            <div class="metric-value">${skill.skillName}</div>
            <div class="metric-label">Skill</div>
          </div>
          <div class="metric">
            <div class="metric-value ${slClass}">${slPercent}%</div>
            <div class="metric-label">Service Level</div>
          </div>
          <div class="metric">
            <div class="metric-value">${skill.contactsQueued}</div>
            <div class="metric-label">In Queue</div>
          </div>
          <div class="metric">
            <div class="metric-value">${formatDuration(skill.longestQueueDuration)}</div>
            <div class="metric-label">Longest Wait</div>
          </div>
          <div class="metric">
            <div class="metric-value">${skill.agentsAvailable} / ${skill.agentsStaffed}</div>
            <div class="metric-label">Available / Staffed</div>
          </div>
        </div>
      `;
    }

    function formatDuration(seconds) {
      const m = Math.floor(seconds / 60);
      const s = seconds % 60;
      return `${m}:${s.toString().padStart(2, "0")}`;
    }

    function updateSkillCard(skillId) {
      const existing = document.getElementById(`skill-${skillId}`);
      if (existing) {
        existing.outerHTML = renderSkillCard(skillData[skillId]);
      }
    }
  </script>
</body>
</html>

Validation, Edge Cases & Troubleshooting

Edge Case 1: API Returns Stale Data (Metrics Not Updating)

CXone’s real-time API has an internal cache refreshed every 15-30 seconds. Polling faster than this returns the same data repeatedly. Don’t poll faster than 15 seconds for REST endpoints. If the WebSocket feed goes silent (no events for >120 seconds), suspect a connection issue - ping the CXone server and verify the heartbeat is being received.

Edge Case 2: Skills Not Appearing in API Response

If a skill has zero activity (no queued contacts, no staffed agents), some CXone API versions omit it from the response. Build your frontend to handle missing skillData entries gracefully by defaulting all metrics to 0 rather than displaying “undefined.”

Edge Case 3: Dashboard Deployed Behind a Corporate Proxy

WebSocket connections (wss://) often fail through HTTP proxies that don’t support WebSocket upgrade headers. Verify connectivity from your deployment environment: wscat -c wss://api.niceincontact.com/... should establish without TLS errors. If the proxy blocks WebSockets, implement a Server-Sent Events (SSE) fallback on your relay server - browsers can receive SSE through most HTTP proxies.

Edge Case 4: Multiple Tenant Support

If your organization manages multiple CXone tenants (e.g., separate BPO accounts), your relay server must maintain separate token pools and WebSocket connections per tenant. Use a tenant ID as a namespace key and route browser clients to the appropriate namespace via URL parameter: ws://relay:8080?tenant=bpo-alpha.


Official References