Paging
Every list on this API pages the same way — a cursor naming the last row you saw, not an offset counting rows you skipped.
Three endpoints return more rows than fit in one response, and all three page identically:
GET /v1/accounts— your usersGET /v1/connections— their authorizationsGET /v1/activities— the workouts you have been granted
GET /v1/activities?limit=100 HTTP/1.1{
"activities": [ "…" ],
"has_more": true,
"next_starting_after": "7e14a9c3-2b60-4f85-9d3a-6c081ef47b52",
"total": 1284
}Send next_starting_after back as starting_after and you get the next page. Keep going
while has_more is true:
let cursor: string | undefined;
do {
const page = await stridee.get('/v1/activities', { since, until, limit: 200, starting_after: cursor });
for (const activity of page.activities) await handle(activity);
cursor = page.next_starting_after;
} while (cursor);starting_after takes the id of the last row you received — a value you already have and
can read in a log line, not an opaque token. It must name a row of yours, or you get a
400: an unknown cursor is a bug in a loop, and answering it with an empty page would look
exactly like reaching the end.
Two fields worth reading carefully
has_more is the paging control, not total. It comes from a row the endpoint fetched
and discarded, so it is exact. Stop when it is false, not when your row count reaches some
number you computed.
total is on the first page only — the page you asked for without a starting_after —
and null on every page after it. It does not change as you page, and counting a quarter of
a million rows again per page is work with no reader. It is the number you put above a
table; it is not part of the loop.
Why there is no offset
Every list here is newest-first over data that keeps arriving. With offset, a workout
landing while you page pushes every row down one, so page two re-serves rows page one
already gave you — and no client should have to be told to dedupe around that. A cursor
names a row rather than a distance from one, so nothing arriving above it moves your
position.
It is also the difference between a backfill that finishes and one that crawls: offset
makes the database walk every row it is skipping, so page 5,000 costs thousands of times
page one. A cursor is one seek whatever page you are on.
If you are re-reading a fixed window — a nightly reconcile — pass both ends:
since=<last run>&until=<this run>, half-open, and next time since is this run's until.
That window cannot change while you walk it, and nothing is counted twice.
Something wrong or missing on this page? Tell us in Discord.