Pagination
Traverse content efficiently with cursors, or request exact numbered pages when the interface truly needs totals.
Posts, post revisions, authors, media, categories, and tags support cursor and numbered pagination. We strongly recommend cursor pagination. It does less database work, stays stable during long traversals, and fits load-more, synchronization, export, build, and agent workflows.
Use numbered pagination only when a person needs to see an exact page number or jump to a known page. Exact totals require additional database work, so numbered requests are slower than cursor requests over the same result set.
Choose a mode
| Need | Parameters | Recommendation |
|---|---|---|
| Load more, sync, export, audit, or process every result | limit and after | Recommended |
| Display “Page 3 of 12” or jump to a page | page and optional per_page | Use only when exact totals improve the interface |
Do not combine cursor and numbered parameters in one request.
Cursor pagination
Start with limit. The default is 20 and the maximum is 100.
curl "https://api.cli-blog.com/v1/posts?locale=en-US&status=published&limit=100" \
--header "x-api-key: $CLI_BLOG_PUBLIC_KEY"The list response contains the items and the position of the next page:
{
"object": "list",
"data": [],
"has_more": true,
"next_cursor": "<opaque-cursor>"
}When has_more is true, send next_cursor as after and keep every other filter, field group, include, locale, and sort value unchanged:
curl "https://api.cli-blog.com/v1/posts?locale=en-US&status=published&limit=100&after=<opaque-cursor>" \
--header "x-api-key: $CLI_BLOG_PUBLIC_KEY"Stop when has_more is false and next_cursor is null.
Why one page stops at 100
The 100-item maximum is a platform safety decision, not a maximum content count. It bounds response bytes, JSON serialization, database work, client memory, and the effect one request can have on other organizations. A larger one-shot response would be slower and less reliable for both the caller and the service.
Retrieve 101 or 100,000 items by following cursors. The page size stays bounded while the total traversal can continue for as many pages as the result set requires.
Traverse every page with REST
This browser-safe example reads published posts with a public key and collects all pages:
const allPosts = [];
let after: string | null = null;
do {
const url = new URL("https://api.cli-blog.com/v1/posts");
url.searchParams.set("status", "published");
url.searchParams.set("locale", "en-US");
url.searchParams.set("fields", "summary");
url.searchParams.set("limit", "100");
if (after) url.searchParams.set("after", after);
const response = await fetch(url, {
headers: { "x-api-key": PUBLIC_CLI_BLOG_KEY },
});
if (!response.ok) throw new Error(`Cli Blog returned ${response.status}`);
const page = await response.json();
allPosts.push(...page.data);
after = page.has_more ? page.next_cursor : null;
} while (after);For large exports, process each page or item as it arrives instead of retaining the entire result set in memory.
Traverse every page with the Node SDK
Cursor-list resources expose paginate(), which follows next_cursor for you:
for await (const post of blog.posts.paginate({
status: "published",
locale: "en-US",
limit: 100,
})) {
await indexPost(post);
}Posts, revisions, authors, media, categories, and tags all provide a cursor iterator in the Node SDK.
Keep cursors with their query
Treat cursor values as opaque. A cursor belongs to its organization, resource, filters, locale, field selection, includes, and sort. Do not decode it, edit it, or reuse it with a different query.
Changing the query while reusing a cursor returns 400 cursor_query_mismatch. A malformed value returns 400 invalid_cursor.
If a traversal must use new filters or a new sort, discard the cursor and start from the first page.
Numbered pagination
Add the required, one-based page parameter. per_page defaults to 20 and has a maximum of 100.
curl "https://api.cli-blog.com/v1/posts?locale=en-US&status=published&page=3&per_page=20" \
--header "x-api-key: $CLI_BLOG_PUBLIC_KEY"Numbered responses add exact metadata for the authorized, filtered result set:
{
"object": "list",
"data": [],
"has_more": true,
"next_cursor": "<opaque-cursor>",
"page": 3,
"per_page": 20,
"total_items": 227,
"total_pages": 12
}Search, locale, workflow, author, category, tag, include/exclude, and relation-match filters are reflected in the exact count. Field groups and includes change the returned object shape, not the count.
If no items match, total_items and total_pages are both 0. A page beyond the final page returns 200 with empty data, the requested page number, the exact totals, has_more: false, and next_cursor: null.
Numbered pages describe the result set at request time. Inserts, deletes, or changes to the sort field between requests can move items between page numbers. Cursor mode is safer for a long-running traversal.
Continue with a cursor after a numbered page
When a numbered response has more results, it also returns next_cursor. To continue efficiently:
- Remove
pageandper_page. - Add
afterwith the returnednext_cursor. - Add
limitand keep the same filters and sort. - Continue until
has_moreis false.
This lets a user begin on an exact numbered page while a background job continues through the remaining results without repeated total counts.
Handle pagination errors
| Request | Response | Fix |
|---|---|---|
per_page without page | 400 invalid_pagination | Add page or use limit |
page or per_page with limit or after | 400 pagination_mode_conflict | Keep only one mode |
| Malformed cursor | 400 invalid_cursor | Restart from the first page |
| Cursor used with another query | 400 cursor_query_mismatch | Keep the original query or restart |
| Number outside the documented range | 422 validation_error | Use a value from 1 through 100 |
Resource-specific filters
Apply pagination after choosing the filters for the resource:
Posts
Combine cursors with workflow, search, relation, field-group, and include controls.
Post revisions
Traverse saved versions for one post.
Authors
Page through public byline profiles.
Media
Traverse the private media inventory.
Categories
Page through localized category terms and optional translations.
Tags
Page through localized tag terms and optional translations.