> ## Documentation Index
> Fetch the complete documentation index at: https://vaquill.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> Offset paging and cursor paging, and which endpoints use which

Eight endpoints page, in two different models.

**Offset paging** walks a fixed window with `limit` and `offset` and stops when `hasMore` is `false`.
Use it for a ranked or ordered list that is not changing while you read it.

**Cursor paging** walks an append-only log by change `id`, forwards with `sinceId` or backwards with `beforeId`.
Use it for change history, where new rows land while you page and an offset would skip or repeat them.

| Endpoint | Model | `limit` default | `limit` max |
| - | - | -: | -: |
| `POST /us/statutes/search` | offset, `offset` 0 to 70 | 10 | 50 |
| `GET /boards` | offset | 500 | 500 |
| `GET /watches` | offset | 100 | 200 |
| `GET /watches/{id}/changes` | cursor, in `meta.cursor` | 50 | 200 |
| `GET /us/statutes/section/{actId}/changes` | cursor, top-level `cursor` | 50 | 200 |
| `GET /us/statutes/section/{actId}/cited-by` | `limit` only | 25 | 100 |
| `GET /us/statutes/section/{actId}/cross-state` | `limit` only | 5 | 25 |
| `GET /us/statutes/section/{actId}/related` | `limit` only | 3 | 10 |

The last three take a `limit` and nothing else.
They return the top N by relevance, and there is no page two, so raise `limit` to its maximum if you want the whole set.
`GET /watches/{id}/deliveries` behaves the same way, with a `limit` of 50 by default and 200 at most.

## Search paging

`POST /us/statutes/search` is the endpoint most integrations page. Two request fields control the window, and one response flag tells you when to stop.

| Field | Type | Default | Range | Purpose |
| - | - | - | - | - |
| `limit` | int | 10 | 1 to 50 | Page size, the number of results returned |
| `offset` | int | 0 | 0 to 70 | Number of results to skip before this page |

### Response fields

Each search response includes these top-level fields:

* `results`: the array of result objects for this page.
* `count`: the number of results returned on **this page**, not the corpus-wide match count.
* `total`: a **deprecated** alias for `count`. It is still returned for back-compatibility. Read `count` in new code.
* `offset`: the offset that was applied.
* `hasMore`: `true` when more results are reachable beyond this page.
* `changedSectionCount`: only when `changedSince` is set. How many sections changed in that window, before ranking. Compare it against `count` to see whether the page is the whole window or a slice of it.
* `creditsConsumed`: what this call actually cost. Read it rather than assuming the list price.

<Warning>
  `count` is a per-page count, not a corpus total. There is no field that reports how many sections in the whole corpus match your query. Use `hasMore` to drive your loop, not arithmetic on `count`.
</Warning>

<Note>
  `hasMore` is always `false` on the last reachable page, because the ranked
  candidate pool is capped at 120 results (`offset` 70 + `limit` 50). It means
  "more results are reachable within that pool", not "more matches exist in the
  corpus". Narrow the query rather than paging deeper.
</Note>

### How deep you can page

The deepest reachable result is `offset + limit`. The full candidate pool is ranked once per query and cached, so every page, including deep ones, is a free slice of that ranking with no extra search or rerank. With the largest page size, `offset` up to `70` reaches the entire reranked pool:

```json theme={"theme":"github-dark"}
{ "query": "data breach notification", "limit": 50, "offset": 70 }
```

<Note>
  If you need to survey a jurisdiction more broadly, scope with `corpusType` and `state` and run several narrower queries rather than trying to page past the window. Tighter scope also gives more predictable pages.
</Note>

### Page until hasMore is false

This loop walks every reachable page, stopping when `hasMore` is `false` or the offset cap is reached.

<CodeGroup>
  ```python Python theme={"theme":"github-dark"}
  import requests

  def search_all(query, corpus_type=None, state=None, page_size=50):
      url = "https://api.vaquill.ai/api/v1/us/statutes/search"
      headers = {
          "Authorization": "Bearer vq_key_...",
          "Content-Type": "application/json",
      }
      results = []
      offset = 0
      while True:
          body = {"query": query, "limit": page_size, "offset": offset}
          if corpus_type:
              body["corpusType"] = corpus_type
          if state:
              body["state"] = state

          data = requests.post(url, headers=headers, json=body).json()
          results.extend(data["results"])

          # Stop when the API says there is no more, or we hit the offset cap.
          if not data["hasMore"] or offset >= 70:
              break
          offset = min(offset + page_size, 70)

      return results

  sections = search_all("data breach notification", corpus_type="STATE", state="ca")
  print(len(sections))
  ```

  ```javascript JavaScript theme={"theme":"github-dark"}
  async function searchAll(query, { corpusType, state, pageSize = 50 } = {}) {
    const url = "https://api.vaquill.ai/api/v1/us/statutes/search";
    const headers = {
      Authorization: "Bearer vq_key_...",
      "Content-Type": "application/json",
    };
    const results = [];
    let offset = 0;

    while (true) {
      const body = { query, limit: pageSize, offset };
      if (corpusType) body.corpusType = corpusType;
      if (state) body.state = state;

      const data = await (
        await fetch(url, { method: "POST", headers, body: JSON.stringify(body) })
      ).json();
      results.push(...data.results);

      if (!data.hasMore || offset >= 70) break;
      offset = Math.min(offset + pageSize, 70);
    }
    return results;
  }

  const sections = await searchAll("data breach notification", {
    corpusType: "STATE",
    state: "ca",
  });
  console.log(sections.length);
  ```
</CodeGroup>

## Offset paging on the list endpoints

`GET /boards` and `GET /watches` both take `limit` and `offset` and return their page inside `data`.

They differ in what they tell you about the rest.
`GET /boards` returns `data.total` and `data.hasMore`, so you can page it properly.
`GET /watches` returns neither: page it by advancing `offset` until a page comes back shorter than `limit`.
In practice one page is always enough there, because the watch cap is 3 outside Business and 100 on Business, and `limit` defaults to 100.

`GET /boards` is the one that bites.
Its `limit` defaults to 500 **and 500 is also the maximum**, so a client that never passes `offset` silently truncates the moment the registry passes 500 boards.
There are about 244 boards today, so the default still covers the whole registry, but that is a fact about today rather than a guarantee.
Read `total` and page on `offset` instead of trusting one call.

```python Python theme={"theme":"github-dark"}
import requests

def all_boards():
    url = "https://api.vaquill.ai/api/v1/boards"
    headers = {"Authorization": "Bearer vq_key_..."}
    boards, offset = [], 0
    while True:
        data = requests.get(
            url, headers=headers, params={"limit": 500, "offset": offset}
        ).json()["data"]
        boards.extend(data["boards"])
        # Stop on the API's own flag, not on len(boards) == total.
        if not data.get("hasMore"):
            break
        offset += 500
    return boards

print(len(all_boards()))
```

`GET /boards` and `GET /watches` are free, so paging them costs nothing but a request against your rate limit.

## Cursor paging on change history

`GET /watches/{id}/changes` and `GET /us/statutes/section/{actId}/changes` page by change `id` rather than by offset.
Change ids are monotonic across the whole corpus, so an id is a stable position in the log even as new rows arrive behind you.

| Parameter | Direction | Purpose |
| - | - | - |
| `sinceId` | forwards | Return changes with an `id` **greater** than this. The polling cursor. |
| `beforeId` | backwards | Return changes with an `id` **less** than this. How you walk into history. |
| `order` | either | `desc` (default) newest first, `asc` when catching up from a cursor. |
| `limit` | either | 1 to 200, default 50. |
| `changeKind` | filter | `added`, `amended` or `removed`. Repeatable. |

The two endpoints return the cursor in different places, which is the one thing to get right:

* `GET /watches/{id}/changes` returns it as `meta.cursor`, alongside `meta.hasMore`.
* `GET /us/statutes/section/{actId}/changes` returns it as a top-level `cursor`, alongside a top-level `hasMore`.

Either way the cursor is the **highest** `id` on the page, and it is `null` on an empty page, so an idle poll must keep the cursor it already holds rather than overwriting it with `null`.

**Polling forwards** is the normal case: save the cursor, and pass it as `sinceId` with `order=asc` on the next call.
Walking forward means a page that fills to `limit` leaves the gap at a known end, so you never lose a row between polls.

**Walking backwards** is what `beforeId` is for: pass the **lowest** `id` you have seen, with the default `order=desc`, to fetch the page of history that sits behind it.
Repeat until a page comes back short of `limit`.

```python Python theme={"theme":"github-dark"}
import requests

def backfill(act_id, headers):
    """Walk a section's change history backwards, oldest page last."""
    url = f"https://api.vaquill.ai/api/v1/us/statutes/section/{act_id}/changes"
    before, out = None, []
    while True:
        params = {"limit": 200, "order": "desc"}
        if before is not None:
            params["beforeId"] = before
        page = requests.get(url, headers=headers, params=params).json()
        changes = page["changes"]
        out.extend(changes)
        if not page["hasMore"] or not changes:
            break
        # Lowest id on this page is where the next page starts.
        before = min(c["id"] for c in changes)
    return out
```

<Warning>
  `hasMore` on both change endpoints means "this page filled to `limit`", not "more rows definitely exist". A page that happens to land exactly on `limit` reports `true` and the next page comes back empty. Treat an empty page as the end.
</Warning>

## Related

<CardGroup cols={2}>
  <Card title="Law Change Alerts" icon="bell" href="/docs/api-guide/alerts">
    Subscribe to a board, then poll its changes with a cursor.
  </Card>

  <Card title="Best practices" icon="circle-check" href="/docs/api-guide/best-practices">
    Scope queries for tighter, more predictable pages.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.