# Query Filter

Shared syntax for filtering, sorting, paginating and grouping results on index endpoints across every Instasent API. Two equivalent formats: URL query string or a JSON _q parameter.

Every index endpoint that returns a list of resources accepts the same Query Filter syntax. You can express the same query in two equivalent formats: compact **URL query string** parameters, or a richer **JSON `_q`** parameter for conditions that need AND/OR logic.

Pick whichever is cleaner for the case at hand — the server parses both into the same internal structure.

> **Note**: Query Filter is the generic filter system used across index endpoints of every Instasent API. It is distinct from [Audience query filter](/product-api/audience-query-filter) and [Audience event query filter](/product-api/audience-event-query-filter), which are separate systems specific to Product API audiences and events with their own syntax and capabilities. Similar names, independent semantics.

## Quickstart

Filter by status:

#### url

```url
GET /api/endpoint?status_eq=estimated
```

#### json

```json
GET /api/endpoint?_q={"filter":[{"field":"status","operator":"eq","value":"estimated"}]}
```

Filter with multiple conditions (combined with AND):

#### url

```url
GET /api/endpoint?number_eq=34600000000&age_gte=35
```

#### json

```json
GET /api/endpoint?_q={"filter":[{"field":"number","operator":"eq","value":"34600000000"},{"field":"age","operator":"gte","value":35}]}
```

## URL query string format

The URL format packs filters, sorting, pagination and grouping into a compact set of query parameters. Use it when conditions combine with AND and the query is small enough to read in a URL.

### Filters

- Field and operator are joined by an underscore: `field_operator=value`.
- Multiple filter parameters combine with `&` — implicit AND.
- Any URL parameter that is not a reserved keyword (`_sort`, `_start`, `_limit`, `_group`, `_q`) is treated as a filter.

```
?number_eq=34600000000&age_gte=35
```

### Sorting

- Parameter: `_sort`.
- Fields separated by commas.
- Direction appended with `:` — `-` for descending, `+` or nothing for ascending (default).

```
?_sort=name,surname:-
```

Sorts by `name` ascending, then `surname` descending.

### Pagination

Parameters `_start` and `_limit`. Both must be set or both omitted.

```
?_start=0&_limit=10
```

### Grouping

For aggregate queries on statistics / reports endpoints.

- Parameter: `_group`.
- The value is not necessarily an existing field — tokens like `hourly`, `daily`, `monthly` are valid.

```
?_group=daily
```

### Nested fields

Reference nested object fields with `*` as the path separator.

```
?age_gte=35&attributes*drink_eq=beer
```

Filters by `age >= 35` AND `attributes.drink == "beer"`.

### Date formats

URL parameter values are strings; the server coerces them based on the field type. For date fields, the accepted formats are:

- **ISO 8601** — `2019-05-08T10:25:12+02:00`
- **Microseconds (UTC)** — `2019-05-08T10:25:12.715`
- **Relative** — `1 day ago`. Valid periods: `second`, `minute`, `hour`, `day`, `month`, `year`.

```
?createdAt_gte=2019-05-08T10:25:12+02:00
?createdAt_gte=1 day ago
```

### Array values

For operators that accept a list of values, separate items with `|`.

```
?age_betweeneq=18|35&name_in=david|daniel|marta
```

Filters by `age` between 18 and 35 (inclusive) AND `name` in `["david", "daniel", "marta"]`.

## JSON `_q` format

For queries that need AND/OR logic or structured conditions, send a JSON object as the `_q` URL parameter.

### Shape

```json
{
  "filter": [
    { "field": "status", "operator": "eq", "value": "estimated" }
  ],
  "paging": { "start": 0, "limit": 20 },
  "sort": [["status", "asc"]],
  "group": "status"
}
```

Top-level keys:

- **`filter`** — array of filter conditions.
- **`paging`** — `{ start, limit }`. Both must be present together.
- **`sort`** — array of `[field, direction]` tuples. Direction is `"asc"` or `"desc"`.
- **`group`** — grouping expression (same semantics as `_group`).

### Filter condition

Each entry in `filter` has three properties:

- **`field`** — field name, or `""` when combining sub-conditions.
- **`operator`** — comparison operator (see [Operators reference](#operators-reference)).
- **`value`** — value to compare against. For list operators this is an array.

### Combining with AND / OR

Nest sub-conditions with an empty `field` and `operator: "and" | "or"`:

```json
{
  "filter": [
    {
      "field": "",
      "operator": "or",
      "value": [
        { "field": "status", "operator": "eq", "value": "draft" },
        { "field": "status", "operator": "eq", "value": "confirmed" }
      ]
    },
    {
      "field": "deliveredText",
      "operator": "eq",
      "value": "Your activation code is 4812"
    }
  ]
}
```

Reads as: `(status == "draft" OR status == "confirmed") AND deliveredText == "Your activation code is 4812"`.

## Operators reference

### Generic

| Operator | Description                      | Field types |
| -------- | -------------------------------- | ----------- |
| `exists` | Field is present. Boolean check. | Any         |

### Comparison

| Operator | Description                   | Field types                  |
| -------- | ----------------------------- | ---------------------------- |
| `eq`     | Equals (exact match).         | String, number, date, id/ref |
| `eqi`    | Equals, case-insensitive.     | String                       |
| `ne`     | Not equals.                   | String, number, date, id/ref |
| `nei`    | Not equals, case-insensitive. | String                       |

### Numeric comparison

| Operator | Description            | Field types  |
| -------- | ---------------------- | ------------ |
| `lt`     | Less than.             | Number, date |
| `lte`    | Less than or equal.    | Number, date |
| `gt`     | Greater than.          | Number, date |
| `gte`    | Greater than or equal. | Number, date |

### List

| Operator | Description                                 | Field types    |
| -------- | ------------------------------------------- | -------------- |
| `in`     | Value is in the list.                       | String, id/ref |
| `ini`    | Value is in the list, case-insensitive.     | String         |
| `nin`    | Value is not in the list.                   | String, id/ref |
| `nini`   | Value is not in the list, case-insensitive. | String         |

List operators take the value as an array.

### String

| Operator     | Description                                   | Field types |
| ------------ | --------------------------------------------- | ----------- |
| `contains`   | Contains substring.                           | String      |
| `containsi`  | Contains substring, case-insensitive.         | String      |
| `ncontains`  | Does not contain substring.                   | String      |
| `ncontainsi` | Does not contain substring, case-insensitive. | String      |
| `starts`     | Starts with.                                  | String      |
| `startsi`    | Starts with, case-insensitive.                | String      |
| `ends`       | Ends with.                                    | String      |
| `endsi`      | Ends with, case-insensitive.                  | String      |

All string operators also accept arrays of values.

### Range

| Operator    | Description                      | Bounds                           |
| ----------- | -------------------------------- | -------------------------------- |
| `range`     | Within range `[min, max]`.       | `min` inclusive, `max` exclusive |
| `between`   | Between `[min, max]`.            | Both exclusive                   |
| `betweeneq` | Between `[min, max]`, inclusive. | Both inclusive                   |

Range operators apply to number and date fields only. Value is always a two-element array.

### Logical

| Operator | Description                                                        |
| -------- | ------------------------------------------------------------------ |
| `and`    | All nested conditions must match. Used with empty `field`.         |
| `or`     | At least one nested condition must match. Used with empty `field`. |

## Common patterns

Filtering by status:

#### url

```url
GET /api/endpoint?status_eq=estimated
```

#### json

```json
GET /api/endpoint?_q={"filter":[{"field":"status","operator":"eq","value":"estimated"}]}
```

Multiple AND conditions:

#### url

```url
GET /api/endpoint?status_eq=confirmed&age_gte=18&age_lte=65
```

#### json

```json
GET /api/endpoint?_q={
  "filter": [
    { "field": "status", "operator": "eq", "value": "confirmed" },
    { "field": "age",    "operator": "gte", "value": 18 },
    { "field": "age",    "operator": "lte", "value": 65 }
  ]
}
```

OR between two conditions:

#### url

```url
GET /api/endpoint?status_eq=draft&status_eq=confirmed
```

#### json

```json
GET /api/endpoint?_q={
  "filter": [
    {
      "field": "",
      "operator": "or",
      "value": [
        { "field": "status", "operator": "eq", "value": "draft" },
        { "field": "status", "operator": "eq", "value": "confirmed" }
      ]
    }
  ]
}
```

> **Warning**: In the URL format, repeating the same `field_op` (e.g. `status_eq=draft&status_eq=confirmed`) is interpreted as OR for that field. Whenever the intent is not obvious at a glance, prefer the JSON format — the explicit `or` operator is easier to read and safer for LLM-generated queries.

Date range:

#### url

```url
GET /api/endpoint?createdAt_gte=2019-05-08T10:25:12%2B02:00&createdAt_lt=2019-06-08T10:25:12%2B02:00
```

#### json

```json
GET /api/endpoint?_q={
  "filter": [
    { "field": "createdAt", "operator": "gte", "value": "2019-05-08T10:25:12+02:00" },
    { "field": "createdAt", "operator": "lt",  "value": "2019-06-08T10:25:12+02:00" }
  ]
}
```

Relative date:

#### url

```url
GET /api/endpoint?createdAt_gte=1 day ago
```

#### json

```json
GET /api/endpoint?_q={"filter":[{"field":"createdAt","operator":"gte","value":"1 day ago"}]}
```

Value in list:

#### url

```url
GET /api/endpoint?name_in=david|daniel|marta
```

#### json

```json
GET /api/endpoint?_q={"filter":[{"field":"name","operator":"in","value":["david","daniel","marta"]}]}
```

Nested fields:

#### url

```url
GET /api/endpoint?attributes*drink_eq=beer
```

#### json

```json
GET /api/endpoint?_q={"filter":[{"field":"attributes.drink","operator":"eq","value":"beer"}]}
```

Filter + sort + paginate together:

#### url

```url
GET /api/endpoint?status_eq=confirmed&_sort=createdAt:-&_start=0&_limit=20
```

#### json

```json
GET /api/endpoint?_q={
  "filter": [{ "field": "status", "operator": "eq", "value": "confirmed" }],
  "sort": [["createdAt", "desc"]],
  "paging": { "start": 0, "limit": 20 }
}
```

## Complete example

A query that filters by `(status == "draft" OR status == "confirmed")` AND a specific delivered text, paginated, sorted and grouped:

```json
{
  "filter": [
    {
      "field": "",
      "operator": "or",
      "value": [
        { "field": "status", "operator": "eq", "value": "draft" },
        { "field": "status", "operator": "eq", "value": "confirmed" }
      ]
    },
    {
      "field": "deliveredText",
      "operator": "eq",
      "value": "Your activation code is 4812"
    }
  ],
  "paging": { "start": 0, "limit": 20 },
  "sort": [["status", "asc"]],
  "group": "status"
}
```
