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

# Pagination

> Walk through search results and the changes feed with cursors: the loop, short and empty pages, and what may change between pages.

Search results and the changes feed come in pages. Each page tells you whether there is more, and gives you a cursor for the next page. The [Pagination reference](https://docs.pinakelai.com/api-reference/pagination.md) defines every field; this guide shows how to use them.

## The loop

1. Send the request with your filters.
2. Process `data`.
3. If `has_more` is `false`, stop.
4. Otherwise send **the same request again**, adding `cursor` set to `next_cursor`, and go to step 2.

The cursor is bound to your filters and sort, so the next request must repeat them. Only `limit`, `include` and `include_total` may change between pages. A cursor sent with different filters gets 400 [`cursor-mismatch`](https://docs.pinakelai.com/errors/cursor-mismatch).

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

import requests

API = "https://api.pinakelai.com/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['PINAKEL_API_KEY']}"


def search_all(params: dict):
    """Yield every record matching `params`, following cursors."""
    params = dict(params, limit=100)
    while True:
        resp = session.get(f"{API}/trademarks", params=params, timeout=30)
        resp.raise_for_status()
        page = resp.json()
        yield from page["data"]
        if not page["has_more"]:
            return
        params["cursor"] = page["next_cursor"]   # filters stay in params


for tm in search_all({"jurisdiction": "AD", "status": "registered", "nice_class": "25"}):
    print(tm["id"], tm["mark"]["text"])
```

```javascript JavaScript theme={null}
const API = "https://api.pinakelai.com/v1";
const headers = { Authorization: `Bearer ${process.env.PINAKEL_API_KEY}` };

// Yield every record matching `filters`, following cursors.
async function* searchAll(filters) {
  const query = new URLSearchParams({ ...filters, limit: "100" });
  while (true) {
    const res = await fetch(`${API}/trademarks?${query}`, { headers });
    if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
    const page = await res.json();
    yield* page.data;
    if (!page.has_more) return;
    query.set("cursor", page.next_cursor); // filters stay in the query
  }
}

for await (const tm of searchAll({ jurisdiction: "AD", status: "registered", nice_class: "25" })) {
  console.log(tm.id, tm.mark.text);
}
```

```bash cURL theme={null}
# First page
curl -G "https://api.pinakelai.com/v1/trademarks" \
  -H "Authorization: Bearer $PINAKEL_API_KEY" \
  --data-urlencode "jurisdiction=AD" \
  --data-urlencode "status=registered" \
  --data-urlencode "nice_class=25" \
  --data-urlencode "limit=100"

# Next page: the same filters, plus the cursor from the previous response
curl -G "https://api.pinakelai.com/v1/trademarks" \
  -H "Authorization: Bearer $PINAKEL_API_KEY" \
  --data-urlencode "jurisdiction=AD" \
  --data-urlencode "status=registered" \
  --data-urlencode "nice_class=25" \
  --data-urlencode "limit=100" \
  --data-urlencode "cursor=$NEXT_CURSOR"
```

## Short and empty pages

Do not use the page size to decide whether you are done. A page can hold fewer records than `limit`, or none at all, while `has_more` is still `true`:

* a broad search returns what each scanned window contains, and some windows are sparse;
* records that cannot be assembled at that moment are left out of the page.

Stop only when `has_more` is `false`. Continuation pages with no records are not billed.

## Consistency between pages

Pages are read live, not from a snapshot. A record that changes while you page can appear on two pages, or move past your position. Treat results as a set keyed by `id`: upsert, don't append. To capture changes that happen during a long walk, take a [changes feed](https://docs.pinakelai.com/guides/syncing.md) cursor before you start and follow it afterwards.

## Cursors

* Treat cursors as opaque strings; their format can change at any time.
* A search cursor is only meant for continuing that search. Store it if you want to resume a long walk later, but do not depend on old search cursors.
* A feed cursor is meant to be stored: it is your position in the feed. Store it after every page.
* A cursor that is damaged, truncated, or signed with a key that has been retired gets 400 [`invalid-cursor`](https://docs.pinakelai.com/errors/invalid-cursor). Restart the search, or for the feed, restart with `since` (see [Syncing](https://docs.pinakelai.com/guides/syncing.md#recovering-a-lost-cursor)).

## Page sizes

| Endpoint | Default `limit` | Maximum |
| - | - | - |
| `GET /v1/trademarks` | 25 | 100, or 25 with `include=full` |
| `GET /v1/changes` | 100 | 100, or 25 with `include=full` |
| `POST /v1/trademarks/batch` | n/a | 100 ids, or 25 with a heavy `include` |

Use `limit=100` for bulk work. Every page is one billed call, whatever its size.


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