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 paginatedPagination applies to every list request whether or not you ask for it. When no
limitis present, 100 items are returned — and 100 is also the maximum. A largerlimitis 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.
| Parameter | Type | What it does |
|---|---|---|
limit | Integer | Page size. Absent or above the maximum, it defaults to 100. |
after_id | String | Return only items after this cursor. Use it to move forward. |
before_id | String | Return only items before this cursor. Use it to move backward. |
Pass either after_id or before_id, not both.
Cursors are opaqueA 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": [...]
}| Field | What it tells you |
|---|---|
has_next_page | Whether more items follow this page. This is your loop condition. |
has_previous_page | Whether items precede this page. |
start_cursor | Cursor marking the first item on this page. Use with before_id. |
end_cursor | Cursor marking the last item on this page. Use with after_id. |
total | Total items matching the request, across all pages. |
limit | The 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 onhas_next_page, not on a short pageA page can contain fewer items than the
limityou 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_pageis 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 do | What actually happens |
|---|---|
Send limit=500 | Silently capped to 100. Check meta.limit if the count looks wrong. |
| Decode or increment the cursor | Breaks as soon as the encoding changes. Send it back verbatim. |
Stop when a page returns fewer than limit | Truncated sync. Stop on has_next_page instead. |
Re-send the same after_id | The same page forever. Always take the cursor from the newest response. |
Send after_id and before_id together | Ambiguous. Use one direction per request. |
| Fetch records one at a time | Burns 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.
Updated 17 days ago

