Skip to main content
Version: API v3.0.0 (Latest)

Pagination

Every list endpoint in 3.0.0 answers with the same envelope and accepts the same four paging parameters. One parser works against all of them.

The envelope​

{
"data": [
{ "id": 1, "name": "Sales Group", "tags": [] }
],
"page": 1,
"page_size": 100,
"num_pages": 3,
"total_items": 214,
"has_next": true,
"has_previous": false,
"next_cursor": "eyJ2IjpbWyJpIiwxMDFdXSwiZCI6ImYiLCJvIjoicGsifQ",
"previous_cursor": null
}
FieldMeaning
dataThe rows on this page
page1-based page number in offset mode; null when walking with a cursor
page_sizeRows on this page
num_pagesTotal pages; null when include_total=false
total_itemsRows matching your filters; null when include_total=false
has_next / has_previousWhether more rows exist in either direction
next_cursor / previous_cursorOpaque cursors to pass back as ?cursor=

A response may also carry filters_applied — see Filtering.

An empty result is still the envelope: data is [], total_items is 0. It never collapses to null or to a bare array, so your parser does not need a special case.

Offset paging​

?page= is the simplest option for a small result set or a numbered UI:

curl "https://apigw.goninjio.com/api/api_gateway/3.0.0/customer/groups/?page=2&page_size=50" \
-H "apiKey: YOUR_API_KEY_HERE"

page_size accepts 1–1000 and defaults to 100.

Cursor paging​

?cursor= is keyset paging, and it is the right choice for walking a whole resource. It does not get slower as you go deeper, and — unlike offset paging — it cannot skip or repeat a row when data changes while you page. With ?page=, a row inserted before your position shifts everything after it, so you silently see one row twice and miss another.

Follow next_cursor until has_next is false:

import requests

headers = {"apiKey": API_KEY}
url = "https://apigw.goninjio.com/api/api_gateway/3.0.0/customer/employees/"
params = {"page_size": 500, "include_total": "false"}

employees = []
while True:
body = requests.get(url, headers=headers, params=params).json()
employees.extend(body["data"])
if not body["has_next"]:
break
params["cursor"] = body["next_cursor"]

Treat cursors as opaque: decode nothing, store nothing long-term, and pass back only what a response gave you. In cursor mode page is null, because a keyset page has no ordinal position to report.

Switching mid-walk

Every response carries a next_cursor, including in offset mode. A client that started with ?page= can switch to cursor paging at any point without starting over.

Skipping the count​

total_items costs a COUNT(*) over your filtered set, which on the larger endpoints can cost more than fetching the page itself. If you are walking every page anyway and do not need the total:

?include_total=false

total_items and num_pages come back as null, and the COUNT(*) is not run — in either paging mode. If nothing in your client displays a total, there is no reason to pay for one.

Which mode should I use?​

SituationUse
Walking an entire resourcecursor, with include_total=false
A UI with numbered pagespage
Jumping to a known offsetpage
A large table that changes while you read itcursor
You need a "showing X of Y" labeleither; keep include_total on

When paging goes wrong​

A paging parameter the endpoint cannot honour is a 400 naming the parameter you sent:

{ "error": "'page_size' must not exceed 1000" }

A cursor is minted against the sort order it was issued for. Replaying one against a different endpoint or a different order is refused rather than served as a plausible-looking wrong page:

{ "error": "Cursor does not match the current ordering" }

A cursor that has been altered in any other way — truncated, re-encoded, or carrying a value that does not belong to the column it sorts on — is refused the same way:

{ "error": "Malformed cursor" }

Both are 400. Pass back only what a response gave you and you will never see either.

A page beyond the last one is not an error. You get the page you asked for, empty:

{ "data": [], "page": 99, "has_next": false, "has_previous": true }

It is not silently answered with the last page, so overrunning is something your client can detect.