> ## Documentation Index
> Fetch the complete documentation index at: https://getsalesio-admin-mcp-wording-for-good-7879419.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination and filtering

> Cursor paging, typed per-field filters, includes, and how objects travel in query strings.

Every list endpoint (`POST /api/{entity}/search`) takes the same four inputs: `filter`, `sort`, `page_size` and `cursor`, plus an optional `include` list.

## Paging

* `page_size` defaults to 50. The ceiling is per endpoint, 100, 200, 500 or 1000 depending on how heavy the table is; each endpoint's reference states its own range, and asking for more than it allows is a `validation_failed` rather than a clamp.
* Responses return `pagination.next_cursor`, an opaque string. Pass it back as `cursor` to get the next page; `null` means the last page. Cursors are forward-only, and there is no offset or page number.
* `page_size: 0` is count-only mode: `items` comes back empty and you read `counts.total_count`. It is the cheapest way to answer "how many match".
* `page_size: 1` is the get-first idiom: one row plus a cursor, for "does anything match, and what is the newest one".

```json theme={null}
{
  "filter": { "status": { "eq": "active" } },
  "page_size": 100,
  "cursor": "eyJpZCI6MTAwfQ=="
}
```

## Filtering

`filter` is a typed object, validated per entity. Each filterable field takes an object of operators; which operators a field supports is spelled out in the reference for that endpoint.

```json theme={null}
{
  "filter": {
    "status": { "in": ["active", "sync_failed"] },
    "created_at": { "gte": "2026-07-01T00:00:00Z" },
    "q": "cooper"
  }
}
```

### The operator vocabulary

Nine operators exist. Which ones a given field accepts is stated in the reference for
that endpoint, and asking for one a field does not accept is an error rather than a
silent no-op.

| Operator    | Takes     | Reads as                                            |
| ----------- | --------- | --------------------------------------------------- |
| `eq`        | a scalar  | equals                                              |
| `ne`        | a scalar  | does not equal                                      |
| `in`        | an array  | matches any of                                      |
| `nin`       | an array  | matches none of                                     |
| `gt`, `gte` | a scalar  | after / at or after (dates), greater than (numbers) |
| `lt`, `lte` | a scalar  | before / at or before, less than                    |
| `is_null`   | a boolean | the column is empty, or is not                      |

Two rules that are easy to miss:

* **Multiple operators on one field are ANDed.** `{"created_at": {"gte": "...", "lt": "..."}}` is a half-open range, which is how you page a window by time rather than by cursor.
* **A bare scalar is shorthand for `eq`.** `{"status": "active"}` and `{"status": {"eq": "active"}}` mean the same thing.

`q` is full-text search where the entity supports it. It is a reserved field inside
`filter`, not a top-level parameter, and each endpoint's description names the columns it
scans.

<Warning>
  An unknown filter field, an unsupported operator, or a value of the wrong type answers
  `validation_failed` with a `field_errors` entry naming the offending path. Nothing is
  silently dropped, so a filter that returns everything is a filter you did not send, not
  one that was ignored.
</Warning>

The response echoes what actually ran in `applied_filters`, which is the fastest way to
confirm a filter reached the backend in the shape you meant.

## Sorting

`sort` is a single object with a `field` and an optional `direction`, for example `{ "field": "created_at", "direction": "desc" }`. Direction defaults to `desc`, and one sort field applies per request. The sortable fields are listed per endpoint.

## Includes

`include` names relations to load with each row, for example `["metrics"]` on API keys. Loaded relations arrive under `items[].included`, keyed by relation name, and the response's top-level `includes` echoes what you asked for. If a requested relation is absent under `included` for a row, that row has none; if it was never in `includes`, it was not requested. The two cases stay distinguishable.

## Objects in query strings

Search runs over `POST` with a JSON body, so this mostly concerns `GET` and `DELETE` endpoints that accept the same parameters:

* An object parameter (`filter`, `sort`) travels as JSON text in the query string: `?filter={"status":{"eq":"active"}}` (URL-encoded).
* An array parameter repeats as `name[]=value`: `?include[]=metrics&include[]=owner`.

The playground on each reference page builds these for you, which is the quickest way to see the exact wire format.
