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

# Pagination

> Lists come a page at a time: pass each page's next_cursor back as cursor until has_more is false.

Every list route returns one page:

**items** `array`

The rows on this page.

**next\_cursor** `string | null`

Pass this back as `cursor` to get the next page. `null` on the last page.

**has\_more** `boolean`

`true` while there are more pages. Loop until it is `false`.

There is **no total row count** on a list. Counting every matching row on every request is the expensive part of a list, so the API does not do it. If you need a count, page to the end and count what you received. The one exception is the work items list, whose `counts` say how many items the whole queue holds in each status.

## Parameters

**limit** `integer`

Rows per page. Leave it out for the route's default. The most a page may hold is listed in [Rate limits](https://docs.citino.ai/api/rate-limits.md). Most routes refuse a larger `limit` with a `400`; the engine searches and claims lists use their maximum instead, and a brands report page can hold fewer rows than you ask for (see [Reports](https://docs.citino.ai/api/reports.md)).

**cursor** `string`

The previous page's `next_cursor`. Leave it out for the first page.

## A cursor belongs to its query

A cursor remembers the filters that produced it. Send it with different parameters (another window, another engine, another `limit`) and the request is refused with `400`, `error.field` set to `cursor`, and the message "this cursor was issued for a different set of filters; request the first page again". That is deliberate: reading on from the wrong place would silently skip rows and hand you a short page that looks complete.

So keep every other parameter exactly the same from page to page, change only `cursor`, and treat the cursor as an opaque string. Do not build one or edit one.

## Short pages, and empty ones

A page can hold fewer rows than `limit` and still be followed by more. On the [report routes](https://docs.citino.ai/api/reports.md) a page can even be empty with `has_more` still `true`, when the days it read held no rows. Never stop because a page was short or empty; stop when `has_more` is `false`.

## Walking every page

```bash curl theme={null}
# The first page, then the next one with the cursor from the first.
curl "https://api.citino.ai/v1/projects/$PROJECT_ID/prompts" -H "Authorization: Bearer $CITINO_API_KEY"
curl "https://api.citino.ai/v1/projects/$PROJECT_ID/prompts?cursor=$NEXT_CURSOR" -H "Authorization: Bearer $CITINO_API_KEY"
```

```python Python theme={null}
import os
import requests

BASE = "https://api.citino.ai/v1"
key = os.environ["CITINO_API_KEY"]
project_id = os.environ["PROJECT_ID"]
session = requests.Session()
session.headers["Authorization"] = f"Bearer {key}"


def every_row(path, **params):
    """Yield every row of a list route, page by page."""
    cursor = None
    while True:
        query = dict(params, **({"cursor": cursor} if cursor else {}))
        response = session.get(f"{BASE}{path}", params=query)
        response.raise_for_status()
        page = response.json()
        yield from page["items"]
        if not page["has_more"]:
            return
        cursor = page["next_cursor"]


for prompt in every_row(f"/projects/{project_id}/prompts"):
    print(prompt["id"], prompt["text"])
```

```javascript JavaScript theme={null}
const BASE = "https://api.citino.ai/v1";
const headers = { Authorization: `Bearer ${process.env.CITINO_API_KEY}` };

// Every row of a list route, page by page.
async function* everyRow(path, params = {}) {
  let cursor = null;
  for (;;) {
    const query = new URLSearchParams({ ...params, ...(cursor ? { cursor } : {}) });
    const response = await fetch(`${BASE}${path}?${query}`, { headers });
    if (!response.ok) throw new Error(`Citino answered ${response.status}`);
    const page = await response.json();
    yield* page.items;
    if (!page.has_more) return;
    cursor = page.next_cursor;
  }
}

for await (const prompt of everyRow(`/projects/${process.env.PROJECT_ID}/prompts`)) {
  console.log(prompt.id, prompt.text);
}
```

The same loop works for CSV: the next cursor then arrives in a header rather than in the body. See [CSV output](https://docs.citino.ai/api/csv.md).

## Related

> [card] [Reports](https://docs.citino.ai/api/reports.md)
>
> Date-first pages, and why a report page can be short.

> [card] [CSV output](https://docs.citino.ai/api/csv.md)
>
> Paging a CSV with the `Citino-Next-Cursor` header.

> [card] [Rate limits](https://docs.citino.ai/api/rate-limits.md)
>
> Page sizes and request budgets.

> [card] [Errors](https://docs.citino.ai/api/errors.md)
>
> What a refused cursor looks like.


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