# Pagination

> Read long lists page by page with `limit` and an opaque `cursor`.

Lists that can grow large use keyset pagination: you ask for a page with `limit`, and the answer tells you
where the next page starts.

## Which operations page

| Operation                                                                                                    | Order                     |
| ------------------------------------------------------------------------------------------------------------ | ------------------------- |
| [`listBatteryModels`](https://passport.kabin.sh/docs/en/api-reference/catalog/list-battery-models)         | By name, then id          |
| [`listBatteries`](https://passport.kabin.sh/docs/en/api-reference/catalog/list-batteries)                  | By serial number, then id |
| [`listEvidence`](https://passport.kabin.sh/docs/en/api-reference/evidence/list-evidence)                   | Newest first              |
| [`listFacilities`](https://passport.kabin.sh/docs/en/api-reference/facilities/list-facilities)             | By name, then id          |
| [`listLifecycleEvents`](https://passport.kabin.sh/docs/en/api-reference/lifecycle/list-lifecycle-events)   | Newest first              |
| [`listPassportVersions`](https://passport.kabin.sh/docs/en/api-reference/passports/list-passport-versions) | Newest first              |

Other lists, such as values, value histories and model versions, return all items in one response.

## Request

- **limit** `integer` (query, default: 50): Number of items per page, from 1 to 200.
- **cursor** `string` (query): The `next_cursor` of the previous page. Leave it out for the first page.

Send the operation's filters (for example `battery_model_id` or `passport_status` of `listBatteries`)
unchanged with every page.

## Response

- **items** `object[]` (required): The items of this page, at most `limit`.
- **next_cursor** `string | null` (required): The cursor for the next page, or `null` when this is the last page.

```json title="200"
{
  "items": [
    { "id": "0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b12", "serial_number": "SV2027-000001" },
    { "id": "0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b13", "serial_number": "SV2027-000002" }
  ],
  "next_cursor": "eyJzIjoiU1YyMDI3LTAwMDAwMiJ9"
}
```

The sample shows only two members of each unit; real items carry the full schema of the operation.

## Rules for cursors

- A cursor is **opaque**. Do not parse it, build it or change it; its content may change at any time.
- A cursor belongs to **the query that produced it**: the same operation, the same path parameters and the
  same filters. Use it right away for the next page; do not keep cursors as long-term bookmarks.
- `next_cursor: null` means there are no more items.
- A cursor that cannot be used is `400 validation_error` on the field `cursor`; a `limit` outside 1 to 200 is
  a `400` on the field `limit`.
- Pages are not a snapshot. Items created or changed while you walk through a list may or may not appear. For
  a consistent copy, page through and then reconcile by id (see
  [Synchronise units](https://passport.kabin.sh/docs/en/guides/sync-units)).

## Read every page

The loops below read all units with a published passport, 200 per page.

```ts title="TypeScript"
type Battery = { id: string; serial_number: string };
type Page<T> = { items: T[]; next_cursor: string | null };

const base = `${process.env.BATTERYPASS_URL}/api/v1`;
const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` };

async function* listBatteries(filters: Record<string, string>): AsyncGenerator<Battery> {
  let cursor: string | null = null;
  do {
    const query = new URLSearchParams({ ...filters, limit: '200' });
    if (cursor) query.set('cursor', cursor);
    const res = await fetch(`${base}/batteries?${query}`, { headers });
    if (!res.ok) throw new Error(`listBatteries: ${res.status} ${await res.text()}`);
    const page: Page<Battery> = await res.json();
    yield* page.items;
    cursor = page.next_cursor;
  } while (cursor);
}

let count = 0;
for await (const unit of listBatteries({ passport_status: 'published' })) {
  console.log(unit.serial_number, unit.id);
  count++;
}
console.log(`${count} units`);
```

```python title="Python"
import os

import requests

base = f"{os.environ['BATTERYPASS_URL']}/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['BATTERYPASS_API_KEY']}"


def list_batteries(**filters):
    params = {**filters, "limit": 200}
    while True:
        res = session.get(f"{base}/batteries", params=params, timeout=30)
        res.raise_for_status()
        page = res.json()
        yield from page["items"]
        if page["next_cursor"] is None:
            return
        params["cursor"] = page["next_cursor"]


count = 0
for unit in list_batteries(passport_status="published"):
    print(unit["serial_number"], unit["id"])
    count += 1
print(count, "units")
```

```go title="Go"
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"time"
)

type battery struct {
	ID           string `json:"id"`
	SerialNumber string `json:"serial_number"`
}

type batteryPage struct {
	Items      []battery `json:"items"`
	NextCursor *string   `json:"next_cursor"`
}

func fetchPage(client *http.Client, query url.Values) (batteryPage, error) {
	var page batteryPage
	req, err := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1/batteries?"+query.Encode(), nil)
	if err != nil {
		return page, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY"))
	res, err := client.Do(req)
	if err != nil {
		return page, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return page, fmt.Errorf("listBatteries: %s", res.Status)
	}
	return page, json.NewDecoder(res.Body).Decode(&page)
}

func main() {
	client := &http.Client{Timeout: 30 * time.Second}
	query := url.Values{"limit": {"200"}, "passport_status": {"published"}}
	count := 0
	for {
		page, err := fetchPage(client, query)
		if err != nil {
			log.Fatal(err)
		}
		for _, unit := range page.Items {
			fmt.Println(unit.SerialNumber, unit.ID)
			count++
		}
		if page.NextCursor == nil {
			break
		}
		query.Set("cursor", *page.NextCursor)
	}
	fmt.Println(count, "units")
}
```

> [!TIP]
> Each page is one request against your organisation's limit of 600 requests per minute. Use `limit=200` for
> bulk reads and see [Rate limits](https://passport.kabin.sh/docs/en/rate-limits) for long walks.
