Analytics API paging loop returning duplicate data on page 2

Just noticed that our SvelteKit server route for fetching queue stats is looping forever. i’m using the Analytics API to pull queue:stat data for the last 24 hours. The first page comes back fine, but the second page contains exactly the same records as page 1. It’s like the pageToken isn’t advancing properly or i’m parsing it wrong.

Here’s the fetch logic in our server function:

const getQueueStats = async () => {
 let allStats = [];
 let nextPageToken = null;
 let pageCount = 0;

 do {
 const params = new URLSearchParams({
 pageSize: 200,
 nextPageToken: nextPageToken,
 interval: 'PT1H',
 query: 'queueId in [12345]'
 });

 const res = await fetch(`https://api.mypurecloud.com/api/v2/analytics/queues/statistics${params.toString()}`);
 const data = await res.json();

 if (data.pageInfo && data.pageInfo.pageToken) {
 nextPageToken = data.pageInfo.pageToken;
 allStats.push(...data.page);
 pageCount++;
 console.log(`Fetched page ${pageCount}, token: ${nextPageToken}`);
 } else {
 nextPageToken = null;
 }

 } while (nextPageToken);

 return allStats;
};

The logs show the token changing every iteration, but the data.page array is identical. i’ve checked the raw JSON response and the pageToken in pageInfo looks like a valid base64 string. Is there a known issue with the Analytics API ignoring subsequent tokens for this specific endpoint? Or am i missing a header?

  1. Checked OAuth token validity - it’s fresh.
  2. Verified the pageSize isn’t hitting a hard limit.

This really shouldn’t be this hard. The docs just say “use the returned token”. i’m stuck.

TL;DR: You’re likely treating the nextPageToken as an offset or timestamp. It’s an opaque string. Stop trying to parse it. Just pass it back verbatim in the pageToken query param.

Check your fetch loop. The Analytics API doesn’t use standard offset pagination. It uses cursor-based pagination via pageToken. If you’re seeing duplicates, you’re probably either:

  1. Ignoring the nextPageToken from the response header/body and generating your own.
  2. Modifying the token string (trimming, URL encoding incorrectly) before the next request.

Here’s how I handle it in my Grafana plugin backend. It’s minimal. No fluff.

async function fetchQueueStats(queueId, startTime, endTime) {
 const allData = [];
 let pageToken = null;
 
 const params = new URLSearchParams({
 'startTime': startTime,
 'endTime': endTime,
 'groupBy': 'queue'
 });

 do {
 if (pageToken) {
 params.set('pageToken', pageToken);
 }

 const response = await fetch(
 `https://api.mypurecloud.com/api/v2/analytics/queues/stats?${params}`,
 {
 headers: {
 'Authorization': `Bearer ${await getAccessToken()}`,
 'Content-Type': 'application/json'
 }
 }
 );

 const data = await response.json();
 
 // CRITICAL: Check if data exists before pushing
 if (data.entities && data.entities.length > 0) {
 allData.push(...data.entities);
 }

 // Get the token for the NEXT request
 pageToken = data.nextPageToken; 

 } while (pageToken);

 return allData;
}

The loop breaks when nextPageToken is null or undefined. If you get the same data, log the pageToken value before and after the request. They should be different strings. If they’re identical, the server thinks you’re asking for page 1 again.

Also, check your startTime and endTime. If they drift or get recalculated inside the loop, you might be re-requesting the same window. Keep the time range static outside the do-while.

I’ve seen this break when people try to “optimize” by caching tokens. Don’t. The token is stateful to that specific query context. Invalidate it on every iteration.

If it’s still looping, dump the raw response headers. Sometimes the Link header is more reliable than the JSON body for the next page, though the body usually works fine for queue:stat.

One more thing: the API returns a max of 5000 entities per page. If you’re hitting that limit and the token doesn’t change, you might have hit a backend bug or rate limit that returns a stale cache. Unlikely, but possible.

i’m in Sydney, so if you need more context, wait for morning here. otherwise, check the token string integrity. it’s usually a simple string handling error.

If I remember right, the analytics endpoint is pretty strict about how you handle that pageToken. It’s not just a simple cursor; it often encodes the time range and entity ID. if you’re stripping parts of it or re-encoding it in your n8n http node, you’ll get duplicates.

in my self-hosted pipelines, i use a function node to parse the response body. check the nextPageToken field directly. if it’s empty, stop the loop. otherwise, pass the raw string to the next iteration’s query params. don’t touch it.

Component Requirement
Node Type HTTP Request (Get)
Query Param pageToken={{ $json.nextPageToken }}
Auth OAuth 2.0 (Client Credentials)

here’s a quick snippet for the function node logic to keep the loop tight:

// Check if pagination should continue
const token = items[0].json.nextPageToken;
if (!token) {
 // End loop
 return []; 
}

// Return single item to trigger next iteration
return [{ json: { pageToken: token } }];

double check your scopes too. analytics:metrics:read is a must. missing that sometimes causes silent failures or weird caching behavior on the server side.

To fix this easily, this is… wrapping the fetch in an OTel span to catch the exact request payload. usually it’s a double-encoding issue. here’s the correct query structure:

{
 "pageToken": "raw_string_from_response",
 "pageSize": 1000
}

stop parsing that token.