Skip to content

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.

The first request uses the normal filters for the operation. For example:

GET /Athletes?fromId=1&count=500

The response supplies two opaque tokens:

  • NextCursor continues towards larger IDs or newer Begin values;
  • PrevCursor continues towards smaller IDs or older Begin values.

Pass the selected token unchanged through the cursor parameter:

GET /Athletes?fromId=1&count=500&cursor=<NextCursor>

For every continuation request:

  1. use the same endpoint;
  2. repeat all selection filters and range parameters from the first request;
  3. add the returned token as cursor;
  4. change only count or 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.

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.

Use an inclusive fromId to scan forward:

GET /Athletes?fromId=1&count=500
GET /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=1000
GET /Athletes?toId=2147483647&count=1000&cursor=<PrevCursor>

This selects the latest items by ID, not by modification time.

Training lists accept inclusive from and to bounds over Begin:

GET /Trainings?origin=Any&from=2026-09-01T00:00:00Z&count=100
GET /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=100
GET /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.

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.

Existing integrations may continue to use the range values returned in the response:

  • replace fromId with NextId to move forward by ID;
  • replace toId with PrevId to move backward by ID;
  • replace from with NextDateTime to move forward by time;
  • replace to with PrevDateTime to move backward by time.

For example, an existing temporal integration may do this:

GET /Trainings?origin=Any&from=2026-09-01T00:00:00Z&count=100
GET /Trainings?origin=Any&from=<NextDateTime>&count=100

To migrate, keep the original filters and boundaries and add the opaque cursor instead:

GET /Trainings?origin=Any&from=2026-09-01T00:00:00Z&count=100
GET /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.

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.

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:

  1. Webhooks for prompt create, update, and delete notifications.
  2. Cursor reads for initial imports and newly visible rows.
  3. 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.

After an interrupted run:

  1. load the last successfully committed cursor and its original request;
  2. retry that request with the same cursor;
  3. upsert the whole page idempotently;
  4. save the returned cursor only after the page is durable;
  5. process queued webhook events and reconcile any uncertain interval.

Never derive a cursor from the result count, last ID, or timestamp yourself.