Pagination

This page contains information about how pagination works in Factorial's API

How to page through list endpoints in Factorial's API.

📘

Every list request is paginated

Pagination applies to every list request whether or not you ask for it. When no limit is present, 100 items are returned — and 100 is also the maximum. A larger limit is capped, not honoured, and no error is raised.

How it works

Factorial uses cursor pagination. Instead of asking for "page 3", you hand back a cursor that marks where the previous page stopped, and the API returns what follows it. Results are always sorted by ID in ascending order.

ParameterTypeWhat it does
limitIntegerPage size. Absent or above the maximum, it defaults to 100.
after_idStringReturn only items after this cursor. Use it to move forward.
before_idStringReturn only items before this cursor. Use it to move backward.

Pass either after_id or before_id, not both.

🚧

Cursors are opaque

A cursor is an encoded string, not a record ID or a page number. Store it and send it back verbatim. Do not parse it, increment it, or build one yourself — the encoding is an implementation detail and may change.

The response

Every paginated response carries a meta block alongside data:

{
  "meta": {
    "has_next_page": true,
    "has_previous_page": false,
    "start_cursor": "MTc=",
    "end_cursor": "MjY=",
    "total": 85,
    "limit": 10
  },
  "data": [...]
}
FieldWhat it tells you
has_next_pageWhether more items follow this page. This is your loop condition.
has_previous_pageWhether items precede this page.
start_cursorCursor marking the first item on this page. Use with before_id.
end_cursorCursor marking the last item on this page. Use with after_id.
totalTotal items matching the request, across all pages.
limitThe page size actually applied — check this if you asked for more than 100.

Paging forward

The first request needs only a limit. Every request after that carries the end_cursor from the response before it, passed as after_id.

First request

GET .../employees/employees?limit=100

Every subsequent request

GET .../employees/employees?limit=100&after_id={end_cursor}

Repeat until has_next_page is false.

cursor = null

loop:
    url = ".../employees/employees?limit=100"
    if cursor is not null:
        url = url + "&after_id=" + cursor

    response = GET url
    process(response.data)

    if response.meta.has_next_page is false:
        break

    cursor = response.meta.end_cursor
❗️

Stop on has_next_page, not on a short page

A page can contain fewer items than the limit you asked for — records deleted between requests simply do not appear. Treating a short page as the end of the list will silently truncate your sync. has_next_page is the only reliable signal.

Paging backward

To walk in the other direction, send the start_cursor of the current page as before_id, and follow has_previous_page instead:

GET .../employees/employees?limit=100&before_id={start_cursor}

Pagination with filters

Filters are applied first, then the result set is paginated, so total reflects the filtered set rather than the whole collection.

Keep every filter identical across the pages of one walk. Changing a filter mid-loop changes the underlying result set, and the cursor you are carrying no longer points where you think it does.

GET .../employees/employees?only_active=true&limit=100&after_id={end_cursor}

Data that changes while you page

Because results are ordered by ascending ID and cursors are positional, a long pagination run is not a frozen snapshot:

  • Records created during the run get higher IDs and appear at the end, so you will pick them up.
  • Records deleted during the run simply do not appear, which is why a page can come back short.
  • Records modified during the run are returned in whatever state they are in when their page is fetched.

For a sync that must be consistent, record the time you started and reconcile changes since then on the next run, rather than assuming one pass saw a stable view.

Counting without downloading

To get a count without pulling the records, request a single item and read meta.total:

GET .../employees/employees?limit=1

Common mistakes

What you doWhat actually happens
Send limit=500Silently capped to 100. Check meta.limit if the count looks wrong.
Decode or increment the cursorBreaks as soon as the encoding changes. Send it back verbatim.
Stop when a page returns fewer than limitTruncated sync. Stop on has_next_page instead.
Re-send the same after_idThe same page forever. Always take the cursor from the newest response.
Send after_id and before_id togetherAmbiguous. Use one direction per request.
Fetch records one at a timeBurns your rate-limit budget. Always page at the maximum size.

Rate limits

Paging at the maximum size of 100 is the cheapest way to read a large collection — it is the difference between 10 requests and 1,000 for the same data. See the FAQs for current rate limits and backoff guidance.


Did this page help you?