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

# Pagination, filtering and sorting

> Page through lists, sort, search and filter.

## Pages

Lists take `page` (1-100,000, default 1) and `perPage` (1-100, default 25) and answer with the page envelope:

```json theme={null}
{ "data": { "data": [], "page": 2, "perPage": 100, "totalPages": 3, "total": 212 } }
```

* `total` is exact; `totalPages` is `ceil(total / perPage)` and `0` when nothing matches.
* A page beyond `totalPages` returns `200` with an empty `data` list.
* There are no cursors or `next` links; compute neighbours from `page` and `totalPages`.
* A `page` or `perPage` outside its range → 400 `data-failed-validation`, constraint [`invalid-page`](https://docs-provider.voucher.allride.io/errors/catalog#invalid-page) with `params.min` / `params.max`.

## Sorting

`sort` is **one** field; prefix it with `-` for descending (`sort=field` or `sort=-field`):

```bash theme={null}
curl -sS "$VOUCHER_API_BASE_URL/provider/v1/voucher-sets?sort=-stock.usagePercent" \
  -H "Authorization: Bearer $VOUCHER_API_KEY"
```

Each list has a default sort and an allow-list of fields (shown on its reference page). The API always appends `id` in the direction of the sort field, so the order is total and stable. Any other value, a comma-separated list included → 400 `data-failed-validation`, constraint [`invalid-sort`](https://docs-provider.voucher.allride.io/errors/catalog#invalid-sort) with `params.allowed` (the sortable fields).

## Searching

Lists that support it take `search` (1-255 characters): a case-insensitive substring match over the fields the reference page names, combined with filters by AND. A number also matches the `id`.

Voucher values are never searched as text: on the vouchers list, `search` is an exact match on the full value, and only for principals with `reveal-provider-vouchers`; others get an empty page.

## Filtering

Filters use bracket notation:

| Form | Meaning |
| - | - |
| `filter[status]=active` | equals (default operator) |
| `filter[status]=active,scheduled` | equals one of (`in`) |
| `filter[redeemedAt][gte]=2026-10-01T00:00:00Z` | operator form |
| `filter[deactivateAt][isNull]=true` | is null |

Operators: `eq`, `ne`, `in`, `gt`, `gte`, `lt`, `lte`, `contains`, `startsWith`, `isNull`. Each list allows specific fields and operators (see its reference page). Rules:

* at most 20 filter parameters per request and 100 values per `in` list (more → constraint `invalid-filter`);
* a literal comma inside a value is written `\,`, a backslash `\\`;
* `null` is not a value; use `isNull`;
* an unknown field or operator, or a value of the wrong type → 400 `data-failed-validation`, constraint [`invalid-filter`](https://docs-provider.voucher.allride.io/errors/catalog#invalid-filter), `property` the parameter (`filter[redeemedAt][gte]`); any other undeclared parameter → the same 400 with the constraint [`unknown-parameter`](https://docs-provider.voucher.allride.io/errors/catalog#unknown-parameter);
* URL-encode the brackets if your HTTP client does not (`filter%5Bstatus%5D=active`).

Voucher set `status` filters use the **effective** status: a scheduled set whose `activeFrom` has passed counts as `active` even before the schedule has run.

## Invalid list parameters

Every problem with the list parameters of one request comes back in **one** 400 [`data-failed-validation`](https://docs-provider.voucher.allride.io/errors/catalog#data-failed-validation), one `more.validationErrors` item per problem:

| Constraint | Parameter | `params` |
| - | - | - |
| [`invalid-page`](https://docs-provider.voucher.allride.io/errors/catalog#invalid-page) | `page` (1-100,000) or `perPage` (1-100) | `min`, `max` |
| [`invalid-sort`](https://docs-provider.voucher.allride.io/errors/catalog#invalid-sort) | `sort` | `allowed`: the sortable fields |
| [`invalid-filter`](https://docs-provider.voucher.allride.io/errors/catalog#invalid-filter) | the `filter[…]` parameter | depends on the filter, for example `maxMonths` |
| [`unknown-parameter`](https://docs-provider.voucher.allride.io/errors/catalog#unknown-parameter) | a parameter the operation does not declare | none |

```json theme={null}
{
  "errors": [
    {
      "message": "Data failed validation",
      "statusCode": 400,
      "errorCode": "data-failed-validation",
      "more": {
        "validationErrors": [
          {
            "property": "sort",
            "constraint": "invalid-sort",
            "params": { "allowed": ["createdAt", "internalName"] },
            "message": { "en": "Sort by one of the allowed fields.", "de": "Sortiere nach einem der erlaubten Felder." }
          }
        ]
      }
    }
  ]
}
```

There is no limit on `page × perPage`, but deep pages are slow and shift while rows are added: narrow the query with filters, or walk time windows as in [Downloading large lists](#downloading-large-lists).

## Syncing large collections

Offset pages can skip or repeat rows when data changes between requests. To keep a local copy in sync, filter by a time field and sort by it, then deduplicate by `id`:

```bash theme={null}
curl -sS -G "$VOUCHER_API_BASE_URL/provider/v1/redemptions" \
  -H "Authorization: Bearer $VOUCHER_API_KEY" \
  --data-urlencode "filter[redeemedAt][gte]=2026-10-22T07:59:00Z" \
  --data-urlencode "sort=redeemedAt" \
  --data-urlencode "perPage=100"
```

Start the next run at the newest `redeemedAt` you saw minus 60 seconds: a redemption can become visible shortly after its timestamp. For a full copy see [Downloading large lists](#downloading-large-lists); for near-real-time updates use [webhooks](https://docs-provider.voucher.allride.io/webhooks/overview.md).

## Downloading large lists

There are no export endpoints: to get a whole list (for a CSV or spreadsheet), page through the list operation itself.

* Use `perPage=100` (the maximum on every list) and send the same filters, `search` and `sort` on every request.
* For a small list, request pages 1 to `totalPages`.
* For a large list (we recommend it above about 100 pages), walk time windows on the list's download key instead, so rows added meanwhile do not shift the pages: `createdAt`, or `redeemedAt` for redemptions. Fix a cut-off at the start time, sort newest first, and after page 100 restart at page 1 with the last row's value as the new upper bound, dropping ids you already have:

```bash theme={null}
curl -sS -G "$VOUCHER_API_BASE_URL/provider/v1/redemptions" \
  -H "Authorization: Bearer $VOUCHER_API_KEY" \
  --data-urlencode "filter[redeemedAt][lte]=2026-10-27T08:00:00Z" \
  --data-urlencode "sort=-redeemedAt" \
  --data-urlencode "perPage=100" \
  --data-urlencode "page=1"
# after page 100: filter[redeemedAt][lte]=<redeemedAt of the last row>, page=1, skip ids already seen
```

* Rows created after the cut-off are not included; run again later for newer rows.
* Send one request at a time. On 429, wait `Retry-After` and repeat the same request. Requests are limited at the network edge per source IP address (see [Limits → Edge limits](https://docs-provider.voucher.allride.io/guides/limits.md#edge-limits)), and a 50,000-row download (500 requests) stays far below that limit.

## Related

* [Requests and responses](https://docs-provider.voucher.allride.io/guides/requests-and-responses.md)
* [Limits](https://docs-provider.voucher.allride.io/guides/limits.md)


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