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="
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
  • hasNextPage: true — there is a next page; pass nextCursor in your next request
  • hasNextPage: false — this is the last page; nextCursor is null
  • nextCursor is 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
1

Subsequent pages — pass the nextCursor value from the previous response:

GET /customers?size=50&sort=createdAt.desc&cursor=eyJmaWVsZCI6ImNyZWF0ZWRBdCIs...
1

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
    }
  }
}
1
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 sort consistent 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.
Contributors: Julien