Skip to main content

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 cursor is rejected with 400 (it is never silently ignored), so a truncated cursor surfaces as an error rather than restarting your loop.
  • limit defaults to 20 and must be an integer between 1 and 100. A value outside that range — 0, negative, non-integer, or greater than 100 — is rejected with 400 (it is not silently clamped).
  • Filtering parameters (e.g. sectorId, costCenterId on /employees) ride the underlying index, so they are safe to combine with pagination — you will never miss a matching record across pages.
  • The name filter (any entity) and the code filter (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=silva matches SILVANA… 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/order parameter. Exception: when you filter by name (any entity) or code (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 is null to 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 (a null cursor 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 with 400. createdSince must be earlier than or equal to createdUntil, otherwise 400.
  • 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 off createdAt — 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 returns 400. (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