Pagination
Pagination
This document explains how pagination is implemented in the Skalin API.
# Overview
The Skalin API uses cursor-based pagination (keyset pagination) as its primary method. Instead of a numeric offset, each page response includes an opaque token (nextCursor) that you pass in the next request to retrieve the following page.
Info
Cursor-based pagination is stateless, efficient on large datasets, and guarantees consistency even if records are added or removed between calls. Offset pagination (page=) remains available but is not recommended for large datasets.
# Supported endpoints
Cursor-based pagination is available on the following routes:
| Method | Route | Resource |
|---|---|---|
| GET | /customers | Customers |
| GET | /contacts | Contacts |
| GET | /agreements | Agreements |
| GET | /customers/:customerId/contacts | Contacts |
| GET | /customers/:customerId/agreements | Agreements |
# Query parameters
| size | integer | Number of results per page. Default: 100. Maximum: 500. |
| cursor | string | Opaque token returned by the previous page. Omit on the first request. Mutually exclusive with page. |
| page | integer | Offset-based page index. Default: 1. Ignored if cursor is present. |
# Response structure
The metadata.pagination object in the response contains the cursor for the next page.
API response with cursor pagination
{
"status": "success",
"data": ["..."],
"metadata": {
"pagination": {
"size": 100,
"hasNextPage": true,
"nextCursor": "eyJmaWVsZCI6ImNyZWF0ZWRBdCIsImRpciI6ImRlc2MiLCJ0aWVicmVha2VycyI6WyJBY21lIiwiMjAyNC0wMS0xMFQxMjowMDowMFoiLCIxMjMiXX0="
}
}
}
2
3
4
5
6
7
8
9
10
11
hasNextPage: true— there is a next page; passnextCursorin your next requesthasNextPage: false— this is the last page;nextCursorisnullnextCursoris an opaque base64url token — do not parse or store it across sessions
# Iterating through pages
First page — omit cursor:
GET /customers?size=50&sort=createdAt.desc
Subsequent pages — pass the nextCursor value from the previous response:
GET /customers?size=50&sort=createdAt.desc&cursor=eyJmaWVsZCI6ImNyZWF0ZWRBdCIs...
Continue until hasNextPage is false.
# Sorting
The sort parameter controls result ordering using the format sort=field.dir (e.g. sort=createdAt.desc). Always use the same sort value across all pages of a single traversal.
# Customers — /customers
| Field | Description |
|---|---|
createdAt | Creation date |
updatedAt | Last update date |
# Contacts — /contacts, /customers/:id/contacts
| Field | Description |
|---|---|
createdAt | Creation date |
updatedAt | Last update date |
# Agreements — /agreements, /customers/:id/agreements
| Field | Description |
|---|---|
mrr | Monthly Recurring Revenue |
arr | Annual Recurring Revenue |
status | Agreement status |
startDate | Start date |
endDate | End date |
engagement | Days remaining before commitment deadline |
notice | Days remaining before notice period |
# Offset pagination
Offset pagination uses the page parameter instead of cursor. It is available on all paginated endpoints but is not recommended for large datasets — performance degrades significantly beyond a few thousand rows.
| page | integer | Page index. Default: 1. Ignored if cursor is present. |
| size | integer | Number of results per page. Default: 100. Maximum: 500. |
The response includes hasNextPage, page and total instead of nextCursor.
API response with offset pagination
{
"status": "success",
"data": ["..."],
"metadata": {
"pagination": {
"size": 50,
"page": 2,
"total": 82,
"hasNextPage": false
}
}
}
2
3
4
5
6
7
8
9
10
11
12
# Null values and ordering
Null values are always sorted last (NULLS LAST) regardless of sort direction. Ordering is deterministic: internal tiebreakers (name, creation date, ID) are applied automatically when multiple records share the same sort value.
# Best practices
- Do not persist cursors across sessions. A cursor encodes sort state and may become invalid if the schema changes.
- Keep
sortconsistent across all pages of a single traversal. - Prefer cursor pagination for large datasets. Offset pagination (
page=) degrades significantly beyond a few thousand rows. - To export all records, loop until
hasNextPage === false.