Pagination
List endpoints return a page of results plus an opaque cursor for the next page:
{
"data": [ /* … up to `limit` items … */ ],
"cursor": "eyJQSyI6ICJ0MSIsIC..."
}
Fetching the next page
Pass the cursor back as a query parameter. When there are no more results, the
response cursor is null.
# First page
curl "https://{host}/v1/employees?limit=50" -H "Authorization: Bearer sk_…"
# Next page — pass the cursor from the previous response
curl "https://{host}/v1/employees?limit=50&cursor=eyJQSyI6…" \
-H "Authorization: Bearer sk_…"
Rules
- Treat the cursor as opaque. It encodes internal paging state; do not parse,
modify, or construct it. Only ever send back a cursor you received — an invalid
or corrupted
cursoris rejected with400(it is never silently ignored), so a truncated cursor surfaces as an error rather than restarting your loop. limitdefaults to20and must be an integer between1and100. A value outside that range —0, negative, non-integer, or greater than100— is rejected with400(it is not silently clamped).- Filtering parameters (e.g.
sectorId,costCenterIdon/employees) ride the underlying index, so they are safe to combine with pagination — you will never miss a matching record across pages. - The
namefilter (any entity) and thecodefilter (cost-centers) are a case-insensitive prefix (begins-with) match, not a substring/contains match — they are anchored to an index sort key. So?name=silvamatchesSILVANA…but not…DA SILVA…. Filter by the leading characters, then narrow client-side if you need a contains search. - Ordering is newest-first for the default list: the most recently created
records come first (descending by creation time). There is no
sort/orderparameter. Exception: when you filter byname(any entity) orcode(cost-centers), results come back in ascending name/code order instead — that filter rides a name/code index whose natural order is alphabetical, not chronological. Either way, page with the cursor until it isnullto read the full set. - No total count. Responses do not include a total/
count; the dataset can be large and counting it is not free. Use the cursor to detect the end (anullcursor means the last page), rather than relying on a total.
Time-window filter (transactions)
The transaction lists — /withdrawals, /returns, /exchanges, /training-records —
accept a createdSince and/or createdUntil filter to fetch only a window:
curl "https://{host}/v1/withdrawals?createdSince=2026-06-01T00:00:00Z&createdUntil=2026-06-30T23:59:59Z&limit=100" \
-H "Authorization: Bearer sk_…"
- Values must be a full ISO-8601 date-time (e.g.
2026-06-01T00:00:00Z). A bare calendar date (2026-06-01) is rejected with400.createdSincemust be earlier than or equal tocreatedUntil, otherwise400. - The window matches the transaction's EVENT date, not the record's
createdAt. It filters the moment the withdrawal/return/exchange was delivered (or the training was completed), which is the business-meaningful timestamp. For data ingested in near-real-time these are within seconds; for back-filled/migrated data they can differ. If you drive an incremental sync, key it off this event window — not offcreatedAt— so you don't miss a late-ingested record whose event was in your window. - The window rides the date index as a key condition, so it is pagination-safe — page through the whole window without missing rows.
- It cannot be combined with a relationship filter (
employeeId,productId,costCenterId,sectorId) in the same request — that combination returns400. (Each routes to a different index; combining them would silently drop the window.)
This is the efficient way to reconcile a webhook gap: fetch exactly the period you may have missed instead of paging the entire history. See Webhooks.
Looping safely
cursor = None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
page = get("/v1/employees", params=params)
process(page["data"])
cursor = page["cursor"]
if not cursor:
break