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

# Pagination, filtering and sorting

> Page through lists with page and perPage, sort, search and filter them, sync large collections incrementally and download large lists.

Every list operation (for example [`GET /consumer/v1/redemptions`](https://docs-consumer.voucher.allride.io/api-reference/consumer-redemptions-list)) takes the same query parameters. Each reference page lists exactly which sort fields, search fields and filters that operation supports.

## Pages

| Parameter | Default | Allowed |
| - | - | - |
| `page` | `1` | 1 to 100,000 |
| `perPage` | `25` | 1 to 100 |

The response is a page envelope:

```json theme={null}
{
  "data": {
    "data": [],
    "page": 3,
    "perPage": 25,
    "totalPages": 5,
    "total": 112
  }
}
```

* `page` and `perPage` echo the values that were applied.
* `total` is the exact number of matching items; `totalPages` is `ceil(total / perPage)` and `0` when nothing matches.
* A `page` beyond `totalPages` is not an error: you get `200` with an empty `data` array and the real totals.
* There are no `next` or `prev` links. Compute the next page from `page` and `totalPages`.
* A `page` or `perPage` outside the allowed range is answered with `400 data-failed-validation`: the `more.validationErrors` item has `property` `page` or `perPage`, the constraint [`invalid-page`](https://docs-consumer.voucher.allride.io/errors/catalog#invalid-page) and `params.min` / `params.max`.

## Sorting

`sort` is **one** field, optionally prefixed with `-` for descending order (`sort=field` or `sort=-field`):

```bash theme={null}
curl -G "$VOUCHER_API_BASE_URL/consumer/v1/redemptions" \
  -H "Authorization: Bearer $VOUCHER_API_KEY" \
  --data-urlencode "sort=-redeemedAt"
```

* Use one of the fields the operation allows. Any other value, a comma-separated list included, is answered with `400 data-failed-validation`: the item has `property` `sort`, the constraint [`invalid-sort`](https://docs-consumer.voucher.allride.io/errors/catalog#invalid-sort) and `params.allowed`, the sortable fields.
* The default is the resource's natural time field, descending (for example `-createdAt` or `-redeemedAt`).
* The server always appends `id` in the direction of your sort field, so the order is total and stable even when several items share a timestamp.
* Sorting by a localised text field uses the language of your request.

## Searching

`search` is a free-text term of 1 to 255 characters (an empty value is ignored).

* It matches case-insensitively as a substring over the operation's search fields, for example a redemption entity's `refId`, `displayName` and `email`.
* A term that is a numeric id, or an exact `refId` or `externalRef`, also matches by equality.
* `search` and filters are combined with AND.

## Filtering

Filters use bracket notation: `filter[<field>][<operator>]=<value>`, or `filter[<field>]=<value>` for the default operator.

| Operator | Meaning | Field types |
| - | - | - |
| `eq` (default) | equals | all |
| `ne` | not equal | text, enum, number, id |
| `in` | one of a comma-separated list | text, enum, number, id |
| `gt`, `gte`, `lt`, `lte` | comparisons | number, timestamp, date, month |
| `contains` | case-insensitive substring | text |
| `startsWith` | case-insensitive prefix | text |
| `isNull` | `true`: is null, `false`: is not null | nullable fields |

Rules:

* Field names are the JSON property names; nested properties use dots (`filter[redemptionEntity.refId]=customer:48213`).
* A value with an unescaped comma means `in`: `filter[status]=completed,voided`. Write a literal comma inside a value as `\,` and a backslash as `\\`.
* `null` is not a value: use `filter[redemptionLinkId][isNull]=true`.
* Several filters are combined with AND; the same field may appear with several operators to form a range.
* At most 20 `filter[…]` parameters per request and 100 values per `in` list; more is answered like an invalid filter (below).
* Timestamps need an offset (`2026-10-01T00:00:00Z`); ranges are half-open, so combine `gte` and `lt`.
* An unknown field, an operator the field does not support or a value of the wrong type is answered with `400 data-failed-validation`: the item's `property` is the parameter (`filter[redeemedAt][gte]`) and its constraint is [`invalid-filter`](https://docs-consumer.voucher.allride.io/errors/catalog#invalid-filter). A query parameter the operation does not declare at all gets the constraint [`unknown-parameter`](https://docs-consumer.voucher.allride.io/errors/catalog#unknown-parameter) in the same response.

Brackets must be URL-encoded. With curl, let `--data-urlencode` do it:

```bash theme={null}
curl -G "$VOUCHER_API_BASE_URL/consumer/v1/redemptions" \
  -H "Authorization: Bearer $VOUCHER_API_KEY" \
  --data-urlencode "filter[redeemedAt][gte]=2026-10-01T00:00:00Z" \
  --data-urlencode "filter[redeemedAt][lt]=2026-11-01T00:00:00Z" \
  --data-urlencode "filter[status]=completed" \
  --data-urlencode "sort=-redeemedAt" \
  --data-urlencode "perPage=100"
```

## Invalid list parameters

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

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

```json theme={null}
{
  "errors": [
    {
      "message": "One or more properties or parameters are invalid.",
      "statusCode": 400,
      "errorCode": "data-failed-validation",
      "more": {
        "validationErrors": [
          {
            "property": "sort",
            "constraint": "invalid-sort",
            "message": { "en": "Sort by one of the allowed fields." },
            "params": { "allowed": ["redeemedAt", "createdAt"] }
          }
        ]
      }
    }
  ]
}
```

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

## Syncing large collections

Page numbers shift when items are added while you page, so an offset walk can skip or repeat items. To keep a local copy in sync:

**1. Filter by a time field and sort by it**

Request `filter[redeemedAt][gte]=<last timestamp you stored>&sort=redeemedAt&perPage=100` (use the time field the operation offers, for example `redeemedAt` or `createdAt`).

**2. Walk the pages**

Read pages until `page` equals `totalPages`. For a long backlog, restart every 100 pages with a new `gte` value: the largest timestamp you have seen.

**3. Deduplicate by id**

`gte` returns the boundary item again; upsert by `id` so repeats do not matter.

To react to changes as they happen, subscribe to [webhooks](https://docs-consumer.voucher.allride.io/webhooks/overview.md).

## Downloading large lists

There are no export endpoints: to get a whole list (for a CSV or spreadsheet file, or a one-off extract), page through the list operation itself. The dashboards' "Download CSV / Excel" action works the same way.

* Send `perPage=100` (the maximum on every list) and the **same** filters, search and sort on every request. For a small list, plain paging from `page=1` to `totalPages` is enough.
* For a large list (we recommend it above about 100 pages), walk time windows on the list's **download key** instead, so rows added while you download do not shift the pages: `createdAt`, or `redeemedAt` for redemptions (each reference page lists the operators; the key supports `gt`, `gte`, `lt`, `lte`).
  1. Fix a cut-off when you start: `filter[redeemedAt][lte]=<start time>` and sort by `-redeemedAt`.
  2. Read pages 1 to 100.
  3. Start again at `page=1` with `filter[redeemedAt][lte]=<redeemedAt of the last row you received>`, and drop rows whose `id` you already have. Stop when a page has fewer than 100 rows.
* Rows created after the cut-off are not included; take them in your next run.
* Send one request at a time. On `429` wait for `Retry-After`, then repeat the same request. Requests are limited at the network edge per source IP address (see [Limits → Edge limits](https://docs-consumer.voucher.allride.io/guides/limits.md#edge-limits)), and a 50,000-row download (500 requests) stays far below that limit.

```bash theme={null}
curl -s "$VOUCHER_API_BASE_URL/consumer/v1/redemptions?perPage=100&page=1&sort=-redeemedAt&filter%5BredeemedAt%5D%5Blte%5D=2026-11-01T06:00:00Z" \
  -H "Authorization: Bearer $VOUCHER_API_KEY"
```

## Related

* [Requests and responses](https://docs-consumer.voucher.allride.io/guides/requests-and-responses.md)
* [Webhooks](https://docs-consumer.voucher.allride.io/webhooks/overview.md)
* [Limits](https://docs-consumer.voucher.allride.io/guides/limits.md)
* [Errors](https://docs-consumer.voucher.allride.io/errors.md)


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