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
}
| Field | Meaning |
|---|---|
data | The rows on this page |
page | 1-based page number in offset mode; null when walking with a cursor |
page_size | Rows on this page |
num_pages | Total pages; null when include_total=false |
total_items | Rows matching your filters; null when include_total=false |
has_next / has_previous | Whether more rows exist in either direction |
next_cursor / previous_cursor | Opaque 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.
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?
| Situation | Use |
|---|---|
| Walking an entire resource | cursor, with include_total=false |
| A UI with numbered pages | page |
| Jumping to a known offset | page |
| A large table that changes while you read it | cursor |
| You need a "showing X of Y" label | either; 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.