Skip to content
Listener.comDevelopers

Pagination

Cursor-based lists with limit, cursor, has_more and next_cursor.

1 min read

Every endpoint that returns many items uses the same list envelope:

JSON
{
  "object": "list",
  "url": "/v1/campaigns",
  "data": [ { "object": "campaign", "id": "cmpA7Hq2LmX9vRt4BzKe" } ],
  "has_more": true,
  "next_cursor": "eyJvIjoyNX0",
  "meta": { "computed_at": "2026-10-14T09:30:00Z", "cache_age_seconds": 12 }
}
Field
dataThis page of results.
has_moretrue when another page exists.
next_cursorPass it as cursor to get the next page. null on the last page.
metaEverything about the list that is not an item: totals, the resolved window, coverage, freshness, or a note explaining an empty list.

Parameters#

Parameter
limitItems per page, 1 to 100. Default 25, or less where an endpoint says so (sponsor matches default to 5).
cursorThe next_cursor from the previous page.

A few lists are ranked results rather than pages: search, suggestions and candidate lists take their own limit and never return a next_cursor.

Cursors are opaque. Do not build or edit them; they may change format at any time. Keep the other query parameters the same across pages, or the cursor may point somewhere that no longer makes sense.

Reading every page#

async function* listAll(path, params = {}) {
  let cursor;
  do {
    const qs = new URLSearchParams({ ...params, limit: '100', ...(cursor ? { cursor } : {}) });
    const res = await fetch(`https://api.listener.com${path}?${qs}`, {
      headers: { Authorization: `Bearer ${process.env.LISTENER_API_KEY}` },
    });
    const page = await res.json();
    yield* page.data;
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);
}

for await (const campaign of listAll('/v1/campaigns', { status: 'active' })) {
  console.log(campaign.id, campaign.title);
}

Paging through a large workspace counts every page against your rate limit. Use limit=100 and filters to keep the number of requests down.