Pagination and synchronization
List operations return bounded pages. Use the cursors returned by the API to continue in either direction, and save progress only after processing a page successfully.
Paginate with opaque cursors
Section titled “Paginate with opaque cursors”The first request uses the normal filters for the operation. For example:
GET /Athletes?fromId=1&count=500The response supplies two opaque tokens:
NextCursorcontinues towards larger IDs or newerBeginvalues;PrevCursorcontinues towards smaller IDs or olderBeginvalues.
Pass the selected token unchanged through the cursor parameter:
GET /Athletes?fromId=1&count=500&cursor=<NextCursor>For every continuation request:
- use the same endpoint;
- repeat all selection filters and range parameters from the first request;
- add the returned token as
cursor; - change only
countor an enrichment flag that the operation explicitly documents as cursor-independent.
Do not parse, create, increment, truncate, or otherwise modify a cursor. A
cursor used with another club, endpoint, path parameters, or set of selection
filters returns 400 Bad Request.
For Training lists, withTelemetry and withUser enrich the selected rows but
do not change selection or ordering. Either flag may therefore change between
continuation requests. Enabling telemetry changes the accepted count limit,
so also adjust count to at most 30. Larger positive values are accepted but
clamped to the applicable maximum by the server.
For temporal lists, the cursor preserves the complete ordering position and all required ID tie-breakers, so items with the same timestamp can be traversed without ambiguity.
Response envelope
Section titled “Response envelope”An abbreviated page looks like this:
{ "NextCursor": "<opaque-next-cursor>", "PrevCursor": "<opaque-prev-cursor>", "Results": [ { "UserId": 300001, "Email": "first.athlete@example.test" } ], "Continue": true}Continue: true means that the page reached the effective count. Continue
with the cursor until Continue is false or an empty page is returned. A
full final page can therefore be followed by an empty one.
Choose the initial range
Section titled “Choose the initial range”Start from an ID
Section titled “Start from an ID”Use an inclusive fromId to scan forward:
GET /Athletes?fromId=1&count=500GET /Athletes?fromId=1&count=500&cursor=<NextCursor>Keep fromId unchanged in every continuation request.
To start from the newest IDs, provide only toId. The first page is returned
in descending order; follow PrevCursor to load older IDs:
GET /Athletes?toId=2147483647&count=1000GET /Athletes?toId=2147483647&count=1000&cursor=<PrevCursor>This selects the latest items by ID, not by modification time.
Start from a date
Section titled “Start from a date”Training lists accept inclusive from and to bounds over Begin:
GET /Trainings?origin=Any&from=2026-09-01T00:00:00Z&count=100GET /Trainings?origin=Any&from=2026-09-01T00:00:00Z&count=100&cursor=<NextCursor>Calendar windows include from and exclude to:
GET /Classes/Calendars/Club?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z&count=100GET /Classes/Calendars/Club?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z&count=100&cursor=<NextCursor>Keep the original date bounds unchanged. A fixed upper bound prevents the cursor from reading beyond that instant.
Complete forward loop
Section titled “Complete forward loop”This server-side JavaScript example stores the cursor only after the page has been processed successfully:
const baseUrl = 'https://api-gw.uptivo.fit/club';
const headers = { 'X-Sub-Key': process.env.UPTIVO_SUBSCRIPTION_KEY, Authorization: `Bearer ${process.env.UPTIVO_CLUB_API_KEY}`, Accept: 'application/json',};
async function syncAthletes() { let cursor = await loadCheckpoint('athletes');
while (true) { const url = new URL(`${baseUrl}/Athletes`); url.searchParams.set('fromId', '1'); url.searchParams.set('count', '1000'); if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`Athlete sync failed with HTTP ${response.status}`); }
const page = await response.json(); const results = page.Results ?? []; const nextCursor = page.NextCursor ?? cursor;
await savePageAndCheckpoint('athletes', results, nextCursor);
if (!page.Continue || results.length === 0) return nextCursor; if (!page.NextCursor) { throw new Error('The API did not return NextCursor for a non-empty page'); }
cursor = page.NextCursor; }}Use idempotent upserts keyed by the Uptivo resource ID. On a network error or
retryable 5xx, retry the same cursor with backoff. Correct the request or
credentials before retrying 400, 401, or 403 responses.
Migrate from range pagination
Section titled “Migrate from range pagination”Existing integrations may continue to use the range values returned in the response:
- replace
fromIdwithNextIdto move forward by ID; - replace
toIdwithPrevIdto move backward by ID; - replace
fromwithNextDateTimeto move forward by time; - replace
towithPrevDateTimeto move backward by time.
For example, an existing temporal integration may do this:
GET /Trainings?origin=Any&from=2026-09-01T00:00:00Z&count=100GET /Trainings?origin=Any&from=<NextDateTime>&count=100To migrate, keep the original filters and boundaries and add the opaque cursor instead:
GET /Trainings?origin=Any&from=2026-09-01T00:00:00Z&count=100GET /Trainings?origin=Any&from=2026-09-01T00:00:00Z&count=100&cursor=<NextCursor>The previous temporal continuation advances only by timestamp. When multiple
items share the same Begin, it can skip items at a page boundary. Opaque
cursors include the resource ID as a tie-breaker and are therefore recommended
for all new integrations and upgrades.
Do not combine the two approaches in one continuation request: when cursor
is present, keep the initial range parameters unchanged.
Coverage and page-size limits
Section titled “Coverage and page-size limits”| Operation | Ordering position | Default | Maximum |
|---|---|---|---|
GET /Athletes |
UserId |
500 | 1,000 |
GET /Contacts |
UserId |
500 | 1,000 |
GET /Subscriptions |
ClassSubscriptionId |
100 | 500 |
GET /UserSubscriptions |
UserClassSubscriptionId |
1,000 | 5,000 |
GET /UserSubscriptions/Active |
UserClassSubscriptionId |
1,000 | 5,000 |
GET /Trainings |
TrainingId or (Begin, TrainingId) |
30; 10 with withTelemetry=true |
100; 30 with withTelemetry=true |
GET /Athletes/{userId}/Trainings |
TrainingId or (Begin, TrainingId) |
30; 10 with withTelemetry=true |
100; 30 with withTelemetry=true |
GET /Classes/Calendars/{calendarType} |
ClassId or (Begin, ClassId) |
100 | 500 |
GET /Challenges |
TrainingChallengeId |
100 | 1,000 |
GET /Challenges/{challengeId}/Results |
UserId, not rank |
100 | 1,000 |
GET /BadgeAwards |
AchievedBadgeId; newest IDs first by default |
100 | 1,000 |
GET /Athletes/{userId}/BadgeAwards |
AchievedBadgeId; newest IDs first by default |
100 | 1,000 |
GET /Athletes/EngagementSummaries |
UserId |
100 | 500 |
GET /Athletes/MembershipSummaries |
UserId |
100 | 500 |
GET /Athletes/AttendanceSummaries |
UserId; no Class date filters |
100 | 500 |
GET /Classes/Bookings |
(ClassBegin, ClassId, UserId) |
100 | 500 |
GET /Classes/{classId}/Bookings |
(ClassBegin, ClassId, UserId) |
100 | 500 |
GET /Athletes/{userId}/Bookings |
(ClassBegin, ClassId, UserId) |
100 | 500 |
The API reference documents how each operation handles a count outside its
accepted range.
Keep synchronized data accurate
Section titled “Keep synchronized data accurate”A cursor enumerates matching rows; it is not a change feed or a frozen snapshot. An ID scan does not reveal updates or deletions, and a temporal scan follows activity time rather than modification time.
Combine three mechanisms:
- Webhooks for prompt create, update, and delete notifications.
- Cursor reads for initial imports and newly visible rows.
- Periodic reconciliation to recover missed events, backdated data, and local processing failures.
Create webhook subscriptions before a bulk import when possible, queue events during the import, and apply them idempotently afterwards. Pages and events can overlap or arrive out of order.
For Classes, reconcile a rolling [from, to) window. For Trainings, use an
overlap appropriate to the expected upload delay. For Athletes, Contacts,
Subscriptions, and UserSubscriptions, periodically repeat the authoritative
collection scan.
Engagement, membership, and attendance summaries can change without creating a new Athlete ID. Membership dates can change the effective state without a write or event. Booking corrections and deletions do not create a new cursor position. Periodically rescan the relevant summary dataset or booking interval and use webhooks for prompt updates. See Bulk summaries and bookings for the endpoint-specific patterns.
Badge award lists enumerate currently visible awards by AchievedBadgeId,
not by AchievedAt. Combine ID imports with Badge.Awarded and reconcile
visibility changes. See Badge awards.
Absence from GET /UserSubscriptions/Active does not mean deletion: an
assignment can be expired, suspended, disabled, or out of activities. Use
GET /UserSubscriptions when reconciling existence.
Recovery checklist
Section titled “Recovery checklist”After an interrupted run:
- load the last successfully committed cursor and its original request;
- retry that request with the same cursor;
- upsert the whole page idempotently;
- save the returned cursor only after the page is durable;
- process queued webhook events and reconcile any uncertain interval.
Never derive a cursor from the result count, last ID, or timestamp yourself.