# Pagination and sorting

This page is for developers who read lists of records from VBAPI. It explains the paging and sorting parameters on list operations, how to read the `X-Pagination` response header, how to fetch every page, and how paging on the Search API differs.

All examples on this page only read data.

## Paging parameters

Every list operation, for example [`GET /groups`](/openapi/vbasoftware/vbasoftware.api/groups/listgroups) or [`GET /subscribers/{subscriberID}/members`](/openapi/vbasoftware/vbasoftware.api/members/listmembers), takes the same three query parameters:

| Parameter | Default | Meaning |
|  --- | --- | --- |
| `page` | `1` | The page to return. Pages start at 1. |
| `pageSize` | `100` | How many records per page. |
| `sortBy` | none | The sort order. See [Sorting](#sorting). |


- **Pages start at 1.** `page=0` returns `500` ("The offset specified in a OFFSET clause may not be negative"), because the API skips `(page - 1) * pageSize` records. Search is different: there, page 0 returns the first page.
- **There is no maximum `pageSize`.** The API returns as many records as you ask for, so choose a size your client can process and that finishes in a reasonable time. Until VBA publishes a recommended ceiling, stay near the default of 100 and page through the results.
- **A page past the end is empty.** Asking for a page beyond `TotalPages` returns an empty `data` array.


## The X-Pagination header

List responses carry their paging details in an `X-Pagination` response header, not in the body. Its value is a JSON object:

```text
X-Pagination: {"TotalCount":230,"TotalPages":3,"CurrentPage":1,"PageSize":100}
```

| Field | Meaning |
|  --- | --- |
| `TotalCount` | The number of records that match, across all pages. |
| `TotalPages` | `TotalCount` divided by `PageSize`, rounded up. It is `0` when nothing matches. |
| `CurrentPage` | The page you asked for. |
| `PageSize` | The page size you asked for. |


HTTP header names are case-insensitive, so `x-pagination` is the same header; some libraries, such as axios, lower-case every name. The field names inside the JSON keep the capitalization shown.

This example reads the first page of 50 groups, sorted by group ID, and prints the paging details. The shared client parses the header into an object for you:

C#
```csharp
        var response = await api.GetAsync("/groups?page=1&pageSize=50&sortBy=group_ID:asc");
        var paging = response.Pagination; // parsed from the X-Pagination header (names are case-insensitive)
        Console.WriteLine($"Page {paging?["CurrentPage"]} of {paging?["TotalPages"]}, " +
                          $"{paging?["PageSize"]} per page, {paging?["TotalCount"]} groups in total");
        foreach (var group in response.Data?.AsArray() ?? new JsonArray())
            Console.WriteLine($"{group?["group_ID"]} {group?["name"]}");
```

Python
```python
        response = api.get("/groups", {"page": 1, "pageSize": 50, "sortBy": "group_ID:asc"})
        paging = response.pagination  # parsed from the X-Pagination header (names are case-insensitive)
        print(f"Page {paging['CurrentPage']} of {paging['TotalPages']}, "
              f"{paging['PageSize']} per page, {paging['TotalCount']} groups in total")
        for group in response.data:
            print(group["group_ID"], group.get("name"))
```

TypeScript
```typescript
  const response = await api.get("/groups", { page: 1, pageSize: 50, sortBy: "group_ID:asc" });
  const paging = response.pagination; // parsed from the X-Pagination header (axios lower-cases header names)
  console.log(
    `Page ${paging?.CurrentPage} of ${paging?.TotalPages}, ${paging?.PageSize} per page, ${paging?.TotalCount} groups in total`,
  );
  for (const group of response.data) console.log(group.group_ID, group.name);
```

## Fetch every page

To read a whole list, request page 1, then keep going while the page number is less than `TotalPages`. The shared client has a helper that does this:

C#
```csharp
    public async IAsyncEnumerable<JsonNode?> GetAllAsync(string path, int pageSize = 100)
    {
        var separator = path.Contains('?') ? '&' : '?';
        for (var page = 1; ; page++) // list pages start at 1
        {
            var response = await GetAsync($"{path}{separator}page={page}&pageSize={pageSize}");
            foreach (var item in response.Data?.AsArray() ?? new JsonArray())
                yield return item;
            var totalPages = response.Pagination?["TotalPages"]?.GetValue<int>() ?? 0;
            if (page >= totalPages)
                yield break;
        }
    }
```

Python
```python
    def iter_pages(self, path, page_size=100, params=None):
        """Yield every item of a list endpoint. List pages start at 1."""
        page = 1
        while True:
            query = dict(params or {}, page=page, pageSize=page_size)
            response = self.get(path, query)
            yield from response.data or []
            pagination = response.pagination
            if not pagination or page >= pagination.get("TotalPages", 0):
                return
            page += 1
```

TypeScript
```typescript
  /** Yield every item of a list endpoint. List pages start at 1. */
  async *getAll<T = any>(path: string, pageSize = 100, params: object = {}): AsyncGenerator<T> {
    for (let page = 1; ; page++) {
      const response = await this.get<T[]>(path, { ...params, page, pageSize });
      for (const item of response.data ?? []) yield item;
      if (page >= (response.pagination?.TotalPages ?? 0)) return;
    }
  }
```

Using it, with a fixed sort order so that records do not move between pages while you read:

C#
```csharp
        var count = 0;
        await foreach (var group in api.GetAllAsync("/groups?sortBy=group_ID:asc", pageSize: 100))
            count++; // process each group here
        Console.WriteLine($"Read {count} groups");
```

Python
```python
        count = 0
        for group in api.iter_pages("/groups", page_size=100, params={"sortBy": "group_ID:asc"}):
            count += 1  # process each group here
        print(f"Read {count} groups")
```

TypeScript
```typescript
  let count = 0;
  for await (const group of api.getAll("/groups", 100, { sortBy: "group_ID:asc" })) {
    count++; // process each group here
  }
  console.log(`Read ${count} groups`);
```

Each page is a separate request and counts toward your [rate limit](/developer-portal/concepts/rate-limits-and-performance). The shared client retries `429` responses with backoff.

## Sorting

`sortBy` takes the spec's format: a comma-separated list of fields, each followed by `:asc` or `:desc`, for example `Property1:desc,Property2:asc`.

- Use the record's field names, for example `sortBy=group_ID:asc` on `GET /groups`. Field names are matched without regard to case.
- Always include the direction. The spec says ascending is the default, but the API rejects a sort field without `:asc` or `:desc` with `400 Invalid Parameter Value`.
- A field the record does not have also returns `400`, with a detail such as `'group_Name' is not a valid sorting property of 'Groups'`.
- Without `sortBy`, the API applies no order, so the order of records is not guaranteed. When you page through a list, sort on the record's key fields (for example `subscriber_ID:asc,member_Seq:asc` for members) so that pages do not overlap or skip records.


## Search paging is different

[`POST /search`](/openapi/vbasoftware/vbasoftware.api/search-request/searchrequest) pages its results with its own rules:

|  | List operations | `POST /search` |
|  --- | --- | --- |
| Where you set paging | Query parameters `page` and `pageSize` | Body fields `page` and `pageCount` |
| Default page size | 100 | 750 |
| Maximum page size | None | 750 for API calls; more is rejected with `400` ("For API calls, PageCount cannot exceed 750 records.") |
| First page | `page=1` | `page` 1. `page` 0 also returns the first page, so a loop that starts at 0 reads the first page twice. |
| `X-Pagination` | Always accurate | Accurate only when the body sets `include_Result_Details: true` and an explicit `pageCount`. Otherwise `TotalCount` is `0`. |
| Sorting | `sortBy` query parameter | `sortPriority` and `sortDescending` on each selected field |


See [Search API](/developer-portal/concepts/search#paging) for a Search paging loop.

## Related

- [Requests and responses](/developer-portal/concepts/requests-and-responses): methods, status codes, and the envelope.
- [Rate limits and performance](/developer-portal/concepts/rate-limits-and-performance): how many pages you can read per second.
- [Search API](/developer-portal/concepts/search): queries across joined tables.