Pagination

Cursor pagination on every list endpoint — limit, cursor, sort, filters, and a walk-the-whole-list recipe.

Updated 2026-06-10

All list endpoints return at most limit rows and a meta.next_cursor value. Pass that cursor on the next request to get the next page.

Why cursors instead of offset

Many APIs paginate with offset and limit — "skip 200 rows, give me the next 25." That model has two problems for the workloads this API serves:

  • Rows shift under you. If a field crew submits three records between your page-2 and page-3 requests, every row moves down by three — an offset walk sees duplicates or silently skips rows. For a sync job whose purpose is "give me every record," skipped rows are data loss.
  • Deep pages get slow. To serve offset=5000 the database fetches and discards 5,000 rows first. The cost grows with depth.

A cursor avoids both. Think of next_cursor as a bookmark: it marks the exact last row you received, and the next request resumes strictly after that row — not at a position that other writes can shift. Concurrent submissions cannot make you skip or repeat rows, and page 200 costs the database the same as page 1.

The trade: there is no "jump to page 7" and no total page count. For exports, syncs, and reports that walk the list front to back, neither is missed.

Your only job as a caller: take meta.next_cursor from each response and pass it back as cursor on the next request, until it comes back null.

Request

GET /api/v1/organizations/{org}/forms/{form}/records?limit=25&cursor=<opaque>
Query paramDefaultMaxNotes
limit25100Positive integer
cursor(none)—Opaque; copy verbatim from the previous response
sortendpoint default—created_at, -created_at, name, -name, etc. The minus prefix sorts descending

Response

{
  "data": [ /* up to `limit` rows */ ],
  "meta": {
    "next_cursor": "eyJpZCI6Ii4uLiJ9",
    "limit": 25
  },
  "request_id": "req_..."
}

When next_cursor is null, you've reached the end.

Cursor opacity

The cursor is base64url-encoded internal state. Treat it as a black box. We may change the encoding without warning. Do not parse, log, or persist cursors beyond the immediate paging session.

Filters

Filters are resource-specific query params and combine with pagination. For example:

GET /api/v1/organizations/{org}/forms/{form}/records
  ?created_after=2026-04-01T00:00:00Z
  &created_before=2026-05-01T00:00:00Z
  &submitted_by=...
  &limit=100

See each resource's reference page for the full filter list.

Walking the whole list

URL="https://fieldscroll.app/api/v1/organizations/$ORG/forms/$FORM/records?limit=100"
while [ -n "$URL" ]; do
  RESP=$(curl -s -H "Authorization: Bearer $FS_KEY" "$URL")
  echo "$RESP" | jq '.data[]'
  CURSOR=$(echo "$RESP" | jq -r '.meta.next_cursor // empty')
  if [ -z "$CURSOR" ]; then break; fi
  URL="https://fieldscroll.app/api/v1/organizations/$ORG/forms/$FORM/records?limit=100&cursor=$CURSOR"
done