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

# Pagination, filtering, and sorting

> Build precise collection queries with page traversal, one-field sorting, UTC windows, and the filter expression language.

Collection endpoints use the same query model. Each operation lists the exact public scalar fields available for filtering and sorting.

## Pagination

| Parameter  | Default | Constraint                      |
| ---------- | ------: | ------------------------------- |
| `page`     |     `1` | One-based integer               |
| `pageSize` |   `100` | Integer from `1` through `1000` |

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --get 'https://api.jobhandy.io/v1/employees' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --data-urlencode 'page=1' \
  --data-urlencode 'pageSize=250'
```

### Reliable page traversal

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart TD
    A[Define tenant and filter window] --> B[Request page 1]
    B --> C[Process items]
    C --> D[Persist checkpoint]
    D --> E{Last page?}
    E -- No --> F[Request next page]
    F --> C
    E -- Yes --> G[Complete run]
```

Offset-style pagination is not a snapshot. Concurrent creates or updates can affect later pages. For repeatable incremental processing, use a stable business checkpoint and an explicit time window rather than assuming pages remain unchanged.

### Page metadata and edge cases

Collection responses return `page`, `pageSize`, `totalPages`, `totalElements`, and `items`. The public contract does not separately define special behavior for an out-of-range page or the exact `totalPages` value when no records match. Use the values in the response rather than hard-coding an assumption.

## Sorting

Use one endpoint-supported scalar field. Prefix it with `-` for descending order.

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
sort=createdAt
sort=-updatedAt
```

Multiple fields and whitespace are rejected. Null values sort first in ascending order and last in descending order.

<Warning>
  A single sort field is not an idempotency or snapshot mechanism. Persist processed resource IDs or source checkpoints where duplicates would be harmful.
</Warning>

## Created-at window

Where documented, `createdAtFrom` and `createdAtTo` are inclusive ISO-8601 UTC timestamps ending in `Z`.

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
createdAtFrom=2026-08-01T00:00:00.000Z
createdAtTo=2026-08-31T23:59:59.999Z
```

## Filter operators

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
=  !=  >  >=  <  <=
IN (...)
CONTAINS
STARTS_WITH
ENDS_WITH
IS NULL
IS NOT NULL
```

`AND` binds before `OR`. Use parentheses when the intended grouping should be explicit.

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
blocked=false AND (countryCode="DE" OR countryCode="AT")
```

## String values and quoting

String comparison is case-insensitive. These forms are supported:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
lastName=Example
lastName="Example"
lastName='Example'
```

Values containing whitespace must be quoted:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
lastName='van Example'
lastName="van Example"
```

Escape an apostrophe with a backslash:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
lastName='O\'Example'
lastName="O\'Example"
```

The examples above show decoded filter expressions. URL-encode the complete query value. `curl --data-urlencode` is recommended.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --get 'https://api.jobhandy.io/v1/employees' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --data-urlencode 'filter=lastName="van Example"'
```

## `IN`, null, and text operators

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
countryCode IN ("DE","AT","CH")
divisionId IS NULL
privateEmail IS NOT NULL
lastName STARTS_WITH "M"
additionalInfo CONTAINS "Building B"
```

## Formal expression model

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
expression  = term, { "OR", term };
term        = factor, { "AND", factor };
factor      = comparison | "(", expression, ")";
comparison  = field, operator, value;
```

The endpoint controls which fields and operators are valid. A syntactically valid filter can still fail with `UNSUPPORTED_FILTER_FIELD` or `UNSUPPORTED_FILTER_OPERATOR`.

## Query-size boundaries

The public contract does not state a separate maximum filter-expression length. Keep expressions bounded, prefer several targeted requests over one excessively complex expression, and handle normal request-validation or payload-limit responses.

## Filter failures

| Error code                    | Cause                                   | Action                                                |
| ----------------------------- | --------------------------------------- | ----------------------------------------------------- |
| `INVALID_FILTER_SYNTAX`       | Expression cannot be parsed             | Check quoting, escaping, parentheses, and `IN` syntax |
| `UNSUPPORTED_FILTER_FIELD`    | Field is not filterable on the endpoint | Use a field listed by the operation                   |
| `UNSUPPORTED_FILTER_OPERATOR` | Operator is not allowed for the field   | Use a compatible operator                             |
| `UNSUPPORTED_SORT_FIELD`      | Sort field is not allowed               | Use one documented scalar field                       |
| `UNKNOWN_QUERY_PARAMETER`     | Query name is not supported             | Remove the unknown parameter                          |
