Pagination

Cursor-based pagination with cursor, limit, and has_more, plus the top-level-array shape used for small lists.

Pagination

List endpoints come in two flavors:

  • Small, bounded resources return their entire result set as a top-level data array with no pagination metadata. Examples: GET /v2/datasets (metadata index) and GET /v2/datasets/{name} (individual dataset rows).
  • Larger resources paginate with opaque cursors. The caller passes cursor and limit; the response returns an inner data array, plus has_more and next_cursor.

Either way, there are no page numbers and no offsets.

Why cursors, not offsets

Page-number pagination ("?page=2&per_page=25") computes slices from a running count. Inserts and deletes during the walk shift every later page — you silently see duplicates or miss rows.

Cursors point at a specific position in the index. Inserts and deletes elsewhere don't move the cursor. The walk always completes with the exact rows that existed when the cursor was minted, regardless of concurrent mutations.

Request parameters

ParameterDefaultMaxMeaning
cursornullOpaque base64 string from the previous response's next_cursor. Omit on the first page.
limit25100Number of items to return. The server may return fewer at the tail of the set.

A few endpoints (like product catalogs) default to limit=250 to return the full set in one response. They're marked x-pagination-eager-default: true in the OpenAPI spec. This is always documented per endpoint — never silent.

Response shape

There are two list-response shapes on the public surface. Which one a
given endpoint uses is documented in the API reference;
the SDKs hide the difference behind a uniform iterator.

Top-level array (no pagination metadata)

Small, bounded resources whose entire result set fits in one response —
GET /v2/datasets (the metadata index) and GET /v2/datasets/{name}
(individual dataset rows) — return data as a top-level array. There
is no has_more, no next_id: the response contains the full
set. The carrier logo endpoint GET /v1/logo/{name} returns image
bytes directly, not JSON, so it isn't strictly a list endpoint.

{
  "object":     "list",
  "livemode":   true,
  "request_id": "req_v2ds_p7q8r9s0t1",
  "data": [
    { "name": "medications", "description": "...", "item_count": 4200 },
    { "name": "conditions",  "description": "...", "item_count": 850  }
  ]
}

Paginated page (cursor + has_more)

For large resources (where row count exceeds the per-page max), the response is a paginated page with cursor metadata. The outer data field contains its own data array (items), has_more, and next_cursor. No public v2 endpoint uses this shape today; the contract is published so SDK iterators are forward-compatible when such endpoints arrive.

{
  "object":     "list",
  "livemode":   true,
  "request_id": "req_01HZK2N5GQR9T8X4B6FJW3Y1AS",
  "data": {
    "data": [
      { "name": "diabetes", "category": "metabolic", "aliases": ["DM"] },
      { "name": "hypertension", "category": "cardiovascular", "aliases": ["HBP"] }
    ],
    "has_more":    true,
    "next_cursor": "Y3Vyc29yX29uZV80NzI="
  }
}

The doubled data is intentional. The outer data holds the standard response envelope. The inner data is the page array. By convention, every paginated list endpoint names its array data — so SDK iterators can walk any list without knowing the resource type.

  • outer data — the response envelope payload.
  • inner data — the resource page (the array of items).
  • has_moretrue if at least one more row exists past next_cursor. false at the tail.
  • next_cursor — opaque base64. Pass it as cursor on the next request. null (or omitted) iff has_more is false.

Two invariants:

  1. has_more === false implies next_cursor is null/absent.
  2. data.length may be less than limit even when has_more === true (if the server's index shard returned a short page). Always trust has_more, not array length, to decide whether to continue.

The cursor is opaque

Treat cursors as opaque string tokens. Never decode, construct, or inspect them. The encoding scheme is internal and may rotate without notice.

Cursors are endpoint-bound: a cursor from one list is not valid on another. Cross-endpoint use returns code: validation_error with param: "/cursor".

Walking a list

The example below walks a hypothetical paginated endpoint
(GET /v1/example/widgets) so the loop shape is concrete; the live v2
surface returns the top-level-array shape and needs no loop.

# curl — manual cursor walk against a paginated endpoint
NEXT=""
while :; do
  if [ -n "$NEXT" ]; then
    URL="https://zyins.isaapi.com/v1/example/widgets?limit=100&cursor=$NEXT"
  else
    URL="https://zyins.isaapi.com/v1/example/widgets?limit=100"
  fi
  RES=$(curl -s "$URL" -H "Authorization: Bearer $ISA_TOKEN")
  echo "$RES" | jq -r '.data.data[].id'
  HAS_MORE=$(echo "$RES" | jq -r '.data.has_more')
  [[ "$HAS_MORE" != "true" ]] && break
  NEXT=$(echo "$RES" | jq -r '.data.next_cursor')
done

For the datasets surface (GET /v3/datasets), no loop is needed — the
endpoint returns the entire requested set in one response, so read
data directly:

curl -s 'https://zyins.isaapi.com/v3/datasets?include=conditions' \
  -H "Authorization: Bearer $ISA_TOKEN" \
  | jq -r '.data.datasets.conditions.items[].name'

What you will NOT find on this API

  • ?page=2 — rejected with validation_error.
  • ?offset=50 — rejected with validation_error.
  • ?starting_after=case_01HZK2P... — use ?cursor=... instead, passing the previous response's next_cursor.
  • A total count. The server does not compute total on list responses. Counting rows is O(N); pagination is O(1) per page. If you need a total, use a dedicated count endpoint (where available) or accept the answer is "unknown until you walk the list."

See also

  • Errorsvalidation_error shape.
  • API reference — every list endpoint declares its
    cursor + limit parameters by $ref to the shared component.