# Read and paginate

> Import a category by following its cursor until hasMore is false, then keep it current with the change feed.

## Page through a category

```bash
curl "https://api.finchnode.com/api/v1/users/$SUBJECT/records/labs?limit=100" \
  -H "Authorization: Bearer $FINCHNODE_API_KEY"
```

Each page has `data`, `hasMore`, `nextCursor`, and `meta`. Send `nextCursor` back as `cursor` until `hasMore` is `false`. Keep the first page's `meta.changeCursor`: you'll need it for the [change feed](/docs/records/change-feed).

Node.js:

```js
async function importCategory(subject, category) {
  const records = [];
  let cursor = null;
  let first;
  for (;;) {
    const url = new URL(`https://api.finchnode.com/api/v1/users/${subject}/records/${category}`);
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);
    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.FINCHNODE_API_KEY}` } });
    if (!res.ok) throw new Error(`FinchNode returned ${res.status}`);
    const page = await res.json();
    first ??= page;
    records.push(...page.data);
    if (!page.hasMore) break;
    cursor = page.nextCursor;
  }
  return { records, changeCursor: first.meta.changeCursor, meta: first.meta };
}
```
Python:

```python
import os
import requests

def import_category(subject, category):
    records, cursor, first = [], None, None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        res = requests.get(
            f"https://api.finchnode.com/api/v1/users/{subject}/records/{category}",
            headers={"Authorization": f"Bearer {os.environ['FINCHNODE_API_KEY']}"},
            params=params,
        )
        if not res.ok:
            raise RuntimeError(f"FinchNode returned {res.status_code}")
        page = res.json()
        first = first or page
        records.extend(page["data"])
        if not page["hasMore"]:
            return {"records": records, "change_cursor": first["meta"]["changeCursor"], "meta": first["meta"]}
        cursor = page["nextCursor"]
```

The Python example raises its own error instead of `raise_for_status()`, because that message includes the URL, and the URL includes the subject.

## Check the import is complete

Before you mark a category imported, read `meta`:

- `syncStatus` is `complete`, not `partial` or `not_started`.
- The category is in `availableCategories` and not in `missingCategories`.

If either fails, store what you got, mark it partial, and read again later. See [Freshness, partial and stale records](/docs/records/freshness).

## Limits

- `limit` is 1 to 100. It defaults to 25.
- Use a cursor only with the endpoint, subject, and category that returned it. FinchNode doesn't always catch a mismatch, and a reused cursor can skip records.
- Don't build or edit cursors. They're opaque.

## Delegated credentials

A [delegated agent credential](/docs/ai-agents/agent-credentials) gets `meta.changeCursor: null` on snapshots. Use `meta.changeCursors[category]`, or the `changeCursor` from a category page.

## List your patients

`GET /users` returns a page of subjects who currently share with your app, with the same `limit` and `cursor`. Use it for admin views and reconciliation, not to find the patient behind a session you just completed. Use `cursor`; `offset` remains only for older integrations.
