Paginate and version Notiondesk API requests

Page through Notiondesk API lists with page_size and cursor, pin a version with the nd-version header, and use snake_case field names.

4 min read

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
}
FieldMeaning
objectAlways list
dataThe items on this page
has_moretrue when another page follows
next_cursorThe value to pass as cursor to get the next page, or null on the last page

Control the page with two query parameters:

ParameterTypeDefaultNotes
page_sizeinteger201 to 100
cursorstringnoneThe 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_size between 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_more is false.
  • 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
VersionStatus
2026-10-16Current
  • Without the header, the request uses the current version.
  • Every response states the version that answered it in its own nd-version header.
  • 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.

Note: Pin 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=10 works. ?pageSize=10 is rejected as an unknown parameter with 400 validation_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 a category. 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?

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.

What's next

Related articles

Was this page helpful?

Still need help? Ask us