Every Notiondesk API list uses the same cursor pagination, and every request can pin the API version your integration was written against with the nd-version header. This article explains how Notiondesk API pagination works, how to read every page of a list, how API versioning works, and the naming rules for fields and parameters.
How Notiondesk API pagination works
Every list route answers the same envelope:
{
"object": "list",
"data": [],
"has_more": false,
"next_cursor": null
}| Field | Meaning |
|---|---|
object | Always list |
data | The items on this page |
has_more | true when another page follows |
next_cursor | The value to pass as cursor to get the next page, or null on the last page |
Control the page with two query parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
page_size | integer | 20 | 1 to 100 |
cursor | string | none | The next_cursor of the previous page, passed back unchanged |
Lists carry no page numbers and no total count.
Read every page of a list
Repeat the request with cursor set to the previous response's next_cursor until has_more is false.
The first page:
curl "https://api.notiondesk.so/sites/c74781f6-1aea-4e17-ab23-bbb8b4d174b6/articles?page_size=100" \
-H "Authorization: Bearer $NOTIONDESK_API_KEY" \
-H "nd-version: 2026-10-16"The next page, with NEXT_CURSOR replaced by the URL-encoded next_cursor of the first page:
curl "https://api.notiondesk.so/sites/c74781f6-1aea-4e17-ab23-bbb8b4d174b6/articles?page_size=100&cursor=NEXT_CURSOR" \
-H "Authorization: Bearer $NOTIONDESK_API_KEY" \
-H "nd-version: 2026-10-16"The same loop in JavaScript:
async function listAll(path) {
const items = [];
let cursor = null;
do {
const url = new URL(`https://api.notiondesk.so${path}`);
url.searchParams.set("page_size", "100");
if (cursor) {
url.searchParams.set("cursor", cursor);
}
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.NOTIONDESK_API_KEY}`,
"nd-version": "2026-10-16",
},
});
const page = await response.json();
items.push(...page.data);
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
return items;
}
const articles = await listAll("/sites/c74781f6-1aea-4e17-ab23-bbb8b4d174b6/articles");And in Python:
import os
import requests
def list_all(path):
items, cursor = [], None
while True:
params = {"page_size": 100}
if cursor:
params["cursor"] = cursor
page = requests.get(
f"https://api.notiondesk.so{path}",
params=params,
headers={
"Authorization": f"Bearer {os.environ['NOTIONDESK_API_KEY']}",
"nd-version": "2026-10-16",
},
).json()
items.extend(page["data"])
if not page["has_more"]:
return items
cursor = page["next_cursor"]Cursor rules
- The cursor is opaque. Do not parse it or build one yourself. Pass it back exactly as you received it.
- You can change
page_sizebetween pages. The next page starts where the previous one ended.
- A cursor holds a position, not a snapshot. An item added or removed while you page can shift the items after it, so a page can repeat or skip one item. If your integration deletes items while it lists them, start again from the first page afterwards.
- Some lists stop early. The changelog list reaches at most the 1,000 newest entries. Page redirects and feedback reach at most the 10,000 newest. Past that limit,
has_moreisfalse.
- Some lists never page. Themes and integrations always return everything in one response, with
has_more: false.
A malformed cursor, or one that points past the end of the list, answers 400 validation_error.
Version your Notiondesk API integration
Send the optional nd-version header to pin the contract version your integration was written against:
nd-version: 2026-10-16| Version | Status |
|---|---|
2026-10-16 | Current |
- Without the header, the request uses the current version.
- Every response states the version that answered it in its own
nd-versionheader.
- An unknown value answers 400
invalid_version.
New routes, new optional parameters and new response fields ship without a new version. Your integration must ignore fields it does not recognize, so a new field never breaks it.
nd-version in production code. Your integration then keeps the behavior it was tested against when a future version changes a response.Naming rules
- Field names are snake_case in requests and responses:
page_size,next_cursor,default_locale.
- Query parameters have one spelling.
?page_size=10works.?pageSize=10is rejected as an unknown parameter with 400validation_error.
- Unknown parameters and body fields are rejected with 400
validation_error, so a typo never passes silently.
- The API uses its own nouns. A help center is a
site, and a collection is acategory. Paths look like/sites/{site_id}/categories.
- Dates are ISO 8601, for example
2026-10-18T14:39:03+00:00.
Common questions
How do I get the total number of articles?
The lists carry no total count. Page through the list and count the items, or call the quick analytics report, which returns the help center's article count. See Get help center analytics with the Notiondesk API.
What is the maximum page size?
100 items. The default is 20. A value outside 1 to 100 answers 400 validation_error.
Do I have to send the nd-version header?
nd-version header?No. Without it, the request uses the current version. Sending it is recommended for production integrations, so their behavior stays fixed.
Will a new response field break my integration?
Not if your code ignores fields it does not recognize. Notiondesk adds response fields without a new version.