# Pagination

List endpoints use **opaque cursor pagination**. There are no page numbers and no
`offset`/`total` — you follow a cursor until it runs out.

## Shape

A list response is:

```json
{
  "items": [ /* … rows … */ ],
  "next_cursor": "eyJrIjoiMjAyNC0wMy0xNVQxNDoyMjowMC4wMDBaIiwiaSI6Ii4uLiIsInYiOjF9"
}
```

- `next_cursor` is a base64url-encoded, **opaque** token. Treat it as a black box — never
  construct, parse, or modify it.
- A `null` `next_cursor` means you've reached the end of the stream.
- To fetch the next page, pass the cursor back **verbatim** as the `cursor` query parameter (or
  `cursor` argument for MCP tools).

## Limits & ordering

| Parameter | Default | Max |
|---|---|---|
| `limit` | 50 | 200 |

Most lists are ordered `(created_at DESC, id DESC)`. The secondary `id` tiebreaker means
results stay stable even when rows share a timestamp, so paging never skips or duplicates a row
under concurrent inserts.

> An unparseable cursor returns `400 pagination.invalid_cursor`. Drop the `cursor` parameter to
> start over.

## Paginated GET loop

The pattern is identical everywhere: start with no cursor, then keep calling with the
`next_cursor` you got back until it's `null`.

**curl** (bash, using `jq`)
```bash
KEY="qk_live_<your-key>"
URL="https://api.qlim8.com/api/v1/emissions?limit=200"
cursor=""
while : ; do
  page=$(curl -s "${URL}${cursor:+&cursor=$cursor}" -H "Authorization: Bearer $KEY")
  echo "$page" | jq -c '.items[]'
  cursor=$(echo "$page" | jq -r '.next_cursor // empty')
  [ -z "$cursor" ] && break
done
```

**TypeScript (fetch)**
```ts
async function* listAll(path: string, key: string) {
  let cursor: string | null = null;
  do {
    const url = new URL(`https://api.qlim8.com/api/v1/${path}`);
    url.searchParams.set("limit", "200");
    if (cursor) url.searchParams.set("cursor", cursor);

    const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
    if (!res.ok) throw new Error(`[${res.status}] ${(await res.json()).code}`);

    const page: { items: unknown[]; next_cursor: string | null } = await res.json();
    yield* page.items;
    cursor = page.next_cursor;
  } while (cursor);
}

for await (const entry of listAll("emissions", process.env.QLIM8_KEY!)) {
  console.log(entry);
}
```

**Python (httpx)**
```python
import os, httpx

def list_all(path: str, key: str):
    cursor = None
    with httpx.Client(headers={"Authorization": f"Bearer {key}"}) as client:
        while True:
            params = {"limit": 200}
            if cursor:
                params["cursor"] = cursor
            resp = client.get(f"https://api.qlim8.com/api/v1/{path}", params=params)
            resp.raise_for_status()
            page = resp.json()
            yield from page["items"]
            cursor = page["next_cursor"]
            if not cursor:
                break

for entry in list_all("emissions", os.environ["QLIM8_KEY"]):
    print(entry)
```

## Date filters

`from` / `to` query parameters are ISO 8601 datetimes and are **inclusive on both ends**,
compared against each record's transaction date:

```
?from=2024-01-01T00:00:00Z&to=2024-12-31T23:59:59Z
```

Combine them freely with `cursor` and `limit`; the filter is applied consistently across pages.
