Pagination
Walk through any list with a cursor. Pages that stay correct while data changes, cost the same at any depth, and a copy-paste loop.
Every list endpoint is cursor-paginated the same way. A cursor marks a position in the list's order, so pages stay correct while data changes between your requests, and a page costs the same however deep it is.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Page size, from 1 to 100. |
cursor | string | none | The meta.nextCursor of the previous page. Leave it out for the first page. |
The meta object
{ "limit": 100, "hasMore": true, "nextCursor": "ZTU2M2ZlYzMtM2Ix…" }| Field | Description |
|---|---|
limit | The page size that was applied. |
hasMore | true if at least one more item exists after this page. |
nextCursor | Pass it as cursor to get the next page. null on the last page. |
How to page
- Request the first page without
cursor. - While
meta.hasMoreistrue, request again withcursor=<meta.nextCursor>. - Stop when
hasMoreisfalse.
Keep every other parameter identical across pages. Changing a filter between pages with the same cursor is not supported.
# First page
curl "https://api-integrations-next.vitruve.fit/v1/workouts?userId=ATHLETE_ID&limit=100" \
-H "X-API-Key: CUSTOMER_INTEGRATION_KEY" -H "X-Partner-Secret: YOUR_PARTNER_SECRET"
# Next page: same filters, plus the cursor from meta.nextCursor
curl "https://api-integrations-next.vitruve.fit/v1/workouts?userId=ATHLETE_ID&limit=100&cursor=NEXT_CURSOR" \
-H "X-API-Key: CUSTOMER_INTEGRATION_KEY" -H "X-Partner-Secret: YOUR_PARTNER_SECRET"async function* listAll(path, params = {}) {
let cursor
do {
const query = new URLSearchParams({ limit: '100', ...params })
if (cursor) query.set('cursor', cursor)
const response = await fetch(`https://api-integrations-next.vitruve.fit/v1${path}?${query}`, {
headers: { 'X-API-Key': customer.vitruveKey, 'X-Partner-Secret': process.env.VITRUVE_PARTNER_SECRET }
})
const body = await response.json()
if (!response.ok) throw new Error(`${response.status}: ${body.message}`)
yield* body.data
cursor = body.meta.hasMore ? body.meta.nextCursor : undefined
} while (cursor)
}
for await (const workout of listAll('/workouts', { userId: 'ATHLETE_ID' })) {
console.log(workout.id, workout.completedAt)
}import os
import requests
def list_all(path, **params):
cursor = None
while True:
query = {"limit": 100, **params}
if cursor:
query["cursor"] = cursor
response = requests.get(
f"https://api-integrations-next.vitruve.fit/v1{path}",
params=query,
headers={"X-API-Key": customer.vitruve_key, "X-Partner-Secret": os.environ["VITRUVE_PARTNER_SECRET"]},
timeout=30,
)
response.raise_for_status()
body = response.json()
yield from body["data"]
if not body["meta"]["hasMore"]:
return
cursor = body["meta"]["nextCursor"]
for workout in list_all("/workouts", userId="ATHLETE_ID"):
print(workout["id"], workout["completedAt"])How a cursor works
A cursor holds the sort value and the id of the last row of a page: a position in the list's order, not a row. So:
- The next page stays correct when that last row is changed, filtered out or deleted before you ask for it.
- New rows don't shift later pages. Rows added before the position never push rows you haven't read yet onto a page you already read.
- A row whose sort date changes moves. A pending workout completed while you page moves to its new place in the order, so you may see it twice, or not in this pass. Store rows by
id. - Depth is free. The thousandth page costs the same as the first.
Rules worth knowing
- Cursors are opaque. Do not parse, build or edit them.
- A cursor belongs to one customer and one list. A cursor served with another customer's key, or for another list (an endpoint sorted another way), answers
400. - Cursors issued before 2026-10 answer
400. Start again from the first page. - Do not stop on a short page. Use
hasMore. A page can be shorter thanlimitonly on the last page, buthasMoreis the contract. - Use
limit=100for exports. It needs the fewest requests against each customer's rate limit. - Order is fixed per endpoint and always ends in
id, so equal sort values are never ambiguous. The default order of each endpoint is stated in the reference.
Errors
| Status | When |
|---|---|
400 | limit outside 1–100 or not an integer, or the cursor is invalid: edited, served with another customer's key or for another list, or issued before 2026-10. |