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).- Unknown query parameters are rejected, and parameter names are case-sensitive.
Any query parameter the endpoint does not recognise returns
400unknown filter parameter "<name>"rather than being ignored — this catches typos (e.g.?Limit=10or?costcenterId=…) early instead of silently returning an unfiltered page. Send only the documented parameters, spelled exactly (camelCase), and do not append tracking params (e.g.utm_*) to API URLs. - 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:00-03:00&createdUntil=2026-06-30T23:59:59.999-03:00&limit=100" \
-H "Authorization: Bearer sk_…"
- Values must be a full ISO-8601 date-time (e.g.
2026-06-01T00:00:00-03:00). A bare calendar date (2026-06-01) or an impossible date (2026-02-30T00:00:00Z) is rejected with400.createdSincemust be earlier than or equal tocreatedUntil, otherwise400. - The timezone offset is honored.
2026-06-01T00:00:00-03:00(midnight in Brasília) and2026-06-01T03:00:00Zare the same instant and return the same rows. A value without an offset is read as UTC, so2026-06-01T00:00:00starts the window at 21:00 of the previous day in Brasília — always send the offset. In a URL, encode a+offset as%2B(an unencoded+can arrive as a space and is rejected with400). - Both bounds are inclusive and exact. To fetch one whole local day, send
createdSince=<day>T00:00:00-03:00&createdUntil=<day>T23:59:59.999-03:00. - 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