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

# Pagination

> All Topo list endpoints return a consistent paginated envelope. Use page, size, sort_by, and filter params to navigate and narrow large result sets.

Every list endpoint in the Topo API returns the same envelope structure so you can handle pagination consistently across all resources. Instead of cursor-based navigation, Topo uses classic page-and-size parameters — straightforward to implement, easy to restart, and safe to cache by page number.

## Response envelope

All list responses wrap their records in a standard pagination envelope:

<ResponseField name="items" type="array" required>
  The records on the current page.
</ResponseField>

<ResponseField name="total_count" type="integer" required>
  Total number of records matching the current query (across all pages).
</ResponseField>

<ResponseField name="total_pages" type="integer" required>
  Total number of pages given the current `size` value.
</ResponseField>

<ResponseField name="has_more" type="boolean" required>
  `true` if there are additional pages after the current one; `false` on the last page.
</ResponseField>

**Example**

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "items": [
    { "id": "018f1a2b-3c4d-7e8f-9a0b-1c2d3e4f5a6b", "email": "alex@example.com" },
    { "id": "019a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "email": "jordan@example.com" }
  ],
  "total_count": 142,
  "total_pages": 15,
  "has_more": true
}
```

## Pagination parameters

Control which page of results you receive using these query parameters on any list endpoint:

<ParamField query="page" default="1" type="integer">
  The page number to retrieve. Must be `1` or greater.
</ParamField>

<ParamField query="size" default="10" type="integer">
  Number of records to return per page. Must be between `1` and `100` inclusive.
</ParamField>

<Note>
  `size` is capped at **100** records per request. To retrieve more than 100 records, increment `page` on successive requests until `has_more` is `false`.
</Note>

## Sorting

Most list endpoints accept `sort_by` and `sort_order` query parameters to control the order of results:

<ParamField query="sort_by" type="string">
  The field name to sort by (e.g. `created_at`, `email`). If omitted, the endpoint uses its default ordering. Refer to individual endpoint documentation for the supported sort fields.
</ParamField>

<ParamField query="sort_order" default="asc" type="string">
  Sort direction. Accepted values: `asc` (ascending) or `desc` (descending).
</ParamField>

## Filtering

Most list endpoints accept additional query parameters to narrow results. Common filter parameters include:

| Parameter        | Type              | Description                                        |
| ---------------- | ----------------- | -------------------------------------------------- |
| `email`          | string            | Filter by exact email address                      |
| `status`         | string            | Filter by record status                            |
| `created_after`  | ISO 8601 datetime | Include only records created after this timestamp  |
| `created_before` | ISO 8601 datetime | Include only records created before this timestamp |

Filter parameters vary by resource — see each endpoint's reference for the full list of supported filters.

<Tip>
  All query parameters use **snake\_case** naming. No aliasing or camelCase variants are accepted.
</Tip>

## Example: paginated request with sorting and filtering

Retrieve the second page of contacts, 25 per page, sorted by most recently created, filtered to a specific email domain:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl 'https://api.topo.io/v1/contacts?page=2&size=25&sort_by=created_at&sort_order=desc' \
  -H "Authorization: Bearer topo_xxxxxxxxxxxx"
```

**Iterate through all records**

To fetch every record matching a query, loop until `has_more` is `false`:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  # Increment page until has_more is false.
  curl -G 'https://api.topo.io/v1/contacts' \
    -d 'page=1' \
    -d 'size=100' \
    -d 'sort_by=created_at' \
    -d 'sort_order=asc' \
    -H "Authorization: Bearer topo_xxxxxxxxxxxx"
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const API_KEY = "topo_xxxxxxxxxxxx";
  const BASE_URL = "https://api.topo.io/v1";

  async function fetchAllContacts() {
    let page = 1;
    const allContacts = [];

    while (true) {
      const response = await fetch(
        `${BASE_URL}/contacts?page=${page}&size=100&sort_by=created_at&sort_order=asc`,
        {
          headers: { Authorization: `Bearer ${API_KEY}` },
        },
      );

      if (!response.ok) {
        throw new Error(`Topo API error: ${response.status}`);
      }

      const data = await response.json();
      allContacts.push(...data.items);

      if (!data.has_more) {
        break;
      }
      page += 1;
    }

    return allContacts;
  }
  ```

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

  API_KEY = "topo_xxxxxxxxxxxx"
  BASE_URL = "https://api.topo.io/v1"

  def fetch_all_contacts():
      page = 1
      all_contacts = []

      while True:
          response = requests.get(
              f"{BASE_URL}/contacts",
              headers={"Authorization": f"Bearer {API_KEY}"},
              params={
                  "page": page,
                  "size": 100,
                  "sort_by": "created_at",
                  "sort_order": "asc",
              },
          )
          response.raise_for_status()
          data = response.json()

          all_contacts.extend(data["items"])

          if not data["has_more"]:
              break
          page += 1

      return all_contacts
  ```
</CodeGroup>

## Envelope stability

The `{ items, total_count, total_pages, has_more }` envelope shape is **frozen** — it will not change within `/v1`. You can safely build your deserialization logic against this structure.
