Pagination

OneAnalytics paginates with opaque cursors on every list endpoint. OFFSET / page=N pagination is deliberately not supported — it produces duplicate and missing rows under any concurrent write, and it scales quadratically.

Request parameters

  • limit — integer, 1 to 500, default 50
  • cursor — opaque base64url string from the previous response's next_cursor, or omit for the first page

Response envelope

{
  "data": [ /* items */ ],
  "page": {
    "limit":       50,
    "next_cursor": "eyJpZCI6ImFiYyIsImNyZWF0ZWRfYXQiOiIyMDI2Li4ufQ",
    "has_more":    true
  }
}

When there are no more pages, next_cursor is null and has_more is false.

Example — list workspaces

# first page
curl "https://api.analytics.rstglobal.in/v1/workspaces?limit=100" \
  -H "Authorization: Bearer $OA_TOKEN"

# next page — paste next_cursor verbatim
curl "https://api.analytics.rstglobal.in/v1/workspaces?limit=100&cursor=eyJp..." \
  -H "Authorization: Bearer $OA_TOKEN"

What's in the cursor

The cursor is opaque — treat it as a black box. Internally, it's a base64url-encoded JSON blob with the last row's primary key plus the sort-order columns (usually created_at, id). This gives us deterministic, consistent pagination: inserts after you started a pagination are simply not seen until you restart; deletes don't cause skips.

Stability guarantees

  • A cursor is valid for 24 hours from issue. Past that, we return 400 pagination/cursor-expired.
  • A cursor is bound to its endpoint and its query string. Reusing a /workspaces cursor on /datasets returns 400 pagination/cursor-mismatch.
  • A cursor survives schema migrations unless the primary key changes (rare).

Client pattern (language-agnostic)

cursor = None
while True:
    resp = client.get("/v1/workspaces", params={"limit": 100, "cursor": cursor}).json()
    for item in resp["data"]:
        handle(item)
    if not resp["page"]["has_more"]:
        break
    cursor = resp["page"]["next_cursor"]

Sorting

List endpoints have a default sort (usually created_at desc, id asc). Alternative sorts are exposed via sort=<field> where supported; the cursor encodes the active sort so you can't accidentally mix two sorts in one pagination.

Why no OFFSET?

  1. CorrectnessOFFSET 1000 over a table that's being inserted into produces duplicates and misses. Cursors read at a stable point.
  2. PerformanceOFFSET is O(N) per page; cursors are O(page_size).
  3. Security — cursor scope-checking prevents a user from skipping to pages that mix tenants.