Phosra returns lists in two shapes, and which one you get depends on the
surface — not on a query flag. Read this once and you will never wonder whether a
response has "more".
Runnable. The shapes below were captured live from
https://phosra-api-sandbox-production.up.railway.app on 2026-07-06. The
catalog reads (/api/v1/platforms, /api/v1/ratings/systems) need no API key.
There is no has_more, no total_count, and no "object": "list" wrapper.
Product lists are bare arrays; census lists carry next_cursor. Do not code
against fields Phosra does not return — detect the last page by the two rules in
the table above.
Offset lists (product data plane)
Most product list endpoints return a plain array and accept limit and, where
supported, offset. An empty result is [], never null.
Offset lists are newest-first and mutable. New rows land at offset 0, so a
row can shift while you page. For an audit-stable walk, filter by a fixed
created_at/since window rather than relying on offsets across a long crawl.
Cursor lists (OCSS census rails)
The OCSS census rails return an envelope with the collection plus a
next_cursor. The cursor is opaque — treat it as a token, never parse it — and
null means you have reached the end. Pass it back verbatim to get the next page.
For the CSM bulk rail the cursor rides the request body (it is part of the
signed, convergent payload — the same request set and cursor always return the same
records):
// Cursor loop: pass next_cursor back until it comes back null.async function* csmBulk(base: string, sign: SignFn, slugs: string[]) { let cursor = ""; for (;;) { const body = JSON.stringify({ slugs, cursor }); const res = await fetch(`${base}/api/v1/csm/reviews/bulk`, { method: "POST", headers: sign("POST", `${base}/api/v1/csm/reviews/bulk`, body), body, }); const { reviews, next_cursor } = await res.json(); yield* reviews; if (next_cursor == null) return; // null ⇒ done cursor = next_cursor; }}
Never construct or mutate a cursor.next_cursor is a server token. Send it
back byte-for-byte; do not decode it, add to it, or assume it is a row id — its
encoding is not part of the contract and can change.
Choosing a page size
Bigger pages, fewer calls
Each request counts against your rate limit. Pull 100 at a
time on offset lists rather than 10 at a time.
Detect the end correctly
Offset lists: a short page ends the walk. Cursor lists: a null
next_cursor ends the walk. Never guess with a count you were not given.