# Audience query filter

Search and segment the unified audience with a structured JSON filter. Combine attribute, event and segment conditions with AND/OR logic, paginate with cursors and run aggregations.

The **Audience Contact Query Filter** is a structured JSON filter posted to `/project/{project}/audience/search` (and a handful of related endpoints) to find contacts by attributes, tags, segment membership or associated events. It is the primary search surface for the unified audience and the building block behind every dynamic segment.

> **Note**: The Audience query filter is distinct from the generic [Query Filter](/further-reading/query-filter) used on index endpoints across every Instasent API. Similar names, independent semantics — this page documents the audience-specific grammar.

## Quickstart

### Contacts with email containing "yahoo"

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_email",
        "operator": "contains",
        "values": ["yahoo"]
      }
    ]
  },
  "limit": 10,
  "offset": 0,
  "sortField": "_email",
  "sortAsc": true,
  "includeAllData": false
}
```

### Contacts with email containing ".com" AND specific tags

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "join": "and",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_email",
        "operator": "contains",
        "values": [".com"]
      },
      {
        "type": "attribute_condition",
        "key": "_client_tags",
        "operator": "matches-string",
        "values": ["&&", "tag-5", "tag-csv-2"]
      }
    ]
  },
  "limit": 100,
  "offset": 0,
  "sortField": "_email",
  "sortAsc": false
}
```

## Basic concepts

### Filter structure

A query filter is an object with:

- **`root`** — the main filter condition, typically a `group` containing multiple conditions.
- **`limit`** — maximum number of results to return.
- **`offset`** — number of results to skip (for pagination).
- **`sortField`** — field to sort by (optional).
- **`sortAsc`** — sort direction: `true` for ascending, `false` for descending (optional).
- **`includeAllData`** — whether to include all contact data or just essential fields (optional, default: `true`).

### Condition types

- **`attribute_condition`** — filter contacts by a contact attribute.
- **`event_condition`** — filter by a field on an associated event.
- **`group_event`** — group event conditions that apply to the same event type.
- **`segment_condition`** — filter by segment membership.
- **`group`** — combine multiple conditions with AND/OR logic (`join: "and" | "or"`).

### Operators at a glance

| Family     | Operators                                                                                                                                          |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Generic    | `match-all`, `match-none`, `exists`, `exists-not`                                                                                                  |
| Boolean    | `matches-bool`                                                                                                                                     |
| String     | `contains`, `contains-not`, `startswith`, `startswith-not`, `endswith`, `endswith-not`, `matches-string`, `matches-string-not`                     |
| Numeric    | `matches-number`, `matches-number-not`, `range-number`, `range-number-not`                                                                         |
| Date       | `matches-date`, `range-date`, `range-date-not`, `range-date-relative`, `range-date-anniversary`, `range-date-dayversary`, `range-date-timeversary` |
| Geographic | `geopoint-distance`                                                                                                                                |
| Segment    | `in-segment`, `in-segment-not` (only on `segment_condition`)                                                                                       |

See the [Operators reference](#operators-reference) at the bottom for the full schemas, `values` shapes and behavioural notes.

## Common patterns

### Filter by email

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_email",
        "operator": "contains",
        "values": [".com"]
      }
    ]
  },
  "limit": 100,
  "offset": 0,
  "sortField": "_email",
  "sortAsc": true
}
```

### Filter by tags

Tags use `matches-string` with a join keyword as the first element of `values`: `&&` (AND) or `||` (OR).

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "join": "and",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_client_tags",
        "operator": "matches-string",
        "values": ["&&", "tag-5", "tag-csv-2"]
      }
    ]
  },
  "limit": 100,
  "offset": 0
}
```

### Filter by segment membership

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "join": "and",
    "children": [
      {
        "type": "segment_condition",
        "key": "test-segment",
        "operator": "in-segment"
      },
      {
        "type": "segment_condition",
        "key": "other-segment",
        "operator": "in-segment-not"
      }
    ]
  },
  "limit": 100,
  "offset": 0
}
```

### Filter by events

Contacts with an email AND an `ecommerce_order_create` event that carries a `campaign` parameter:

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_email",
        "operator": "exists",
        "values": []
      },
      {
        "type": "group_event",
        "datasource": ["datasource-id-1", "datasource-id-2"],
        "event": "ecommerce_order_create",
        "children": [
          {
            "type": "event_condition",
            "key": "campaign",
            "operator": "exists",
            "values": []
          }
        ]
      }
    ]
  },
  "limit": 10,
  "offset": 0,
  "sortField": "_email",
  "sortAsc": true
}
```

### Date range queries

Contacts imported in the last 7 days:

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_date_imported",
        "operator": "range-date-relative",
        "values": {
          "lowerOffset": -7,
          "upperOffset": null,
          "lowerOffsetPeriod": "day",
          "upperOffsetPeriod": "day"
        }
      }
    ]
  },
  "limit": 100,
  "offset": 0
}
```

### Complex event filtering

Contacts with `transactional_send` events (SMS) from the last 30 days **OR** with the `marketing` category:

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_email",
        "operator": "exists",
        "values": []
      },
      {
        "type": "group_event",
        "event": "transactional_send",
        "join": "and",
        "datasource": ["datasource-id-1", "datasource-id-2"],
        "children": [
          {
            "type": "event_condition",
            "key": "type",
            "operator": "matches-string",
            "values": ["sms"]
          },
          {
            "type": "group",
            "join": "or",
            "children": [
              {
                "type": "event_condition",
                "key": "created-at",
                "operator": "range-date-relative",
                "values": {
                  "lowerOffset": -30,
                  "lowerOffsetPeriod": "day",
                  "upperOffset": 0,
                  "upperOffsetPeriod": "day"
                }
              },
              {
                "type": "event_condition",
                "key": "category",
                "operator": "matches-string",
                "values": ["marketing"]
              }
            ]
          }
        ]
      }
    ]
  },
  "limit": 10
}
```

### Additional filter options

Top-level filters that refine the result set without going through `root`:

| Field                   | Effect                                                       |
| ----------------------- | ------------------------------------------------------------ |
| `filterAudienceIds`     | Restrict to specific audience contact IDs.                   |
| `filterNotAudienceIds`  | Exclude specific audience contact IDs.                       |
| `filterContactIds`      | Restrict to specific datasource contact IDs.                 |
| `filterDatasourceIds`   | Restrict to specific datasource IDs.                         |
| `filterUniversalSearch` | Search within the universal search field.                    |
| `filterSamplingPercent` | Retrieve only a percentage of results (0–100, default: 100). |
| `filterBucketMin`       | Minimum bucket number (0–199, default: 0).                   |
| `filterBucketMax`       | Maximum bucket number (0–199, default: 199).                 |
| `filterBucketIn`        | Restrict to specific bucket numbers (array).                 |
| `filterBucketNotIn`     | Exclude specific bucket numbers (array).                     |

Example combining universal search and sampling:

```json
{
  "version": "0.0.1",
  "limit": 100,
  "offset": 0,
  "sortField": "_email",
  "sortAsc": true,
  "filterUniversalSearch": ".com",
  "filterSamplingPercent": 33,
  "root": {
    "type": "group",
    "join": "and",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_email",
        "operator": "contains",
        "values": [".com"]
      }
    ]
  }
}
```

## Dynamic attributes

These are not project attributes but can be used as `key` in `attribute_condition`:

- `_universal_search` — universal search field across common identifying attributes.
- `_bucket` — bucket number (0–199), for deterministic sampling.

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "join": "and",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_universal_search",
        "operator": "contains",
        "values": ["brittney"]
      },
      {
        "type": "attribute_condition",
        "key": "_bucket",
        "operator": "range-number",
        "values": {
          "lowerNumber": 0,
          "upperNumber": 40
        }
      }
    ]
  },
  "limit": 100,
  "offset": 0,
  "sortField": "_email",
  "sortAsc": true
}
```

### Sampling with buckets

Bucket ranges are deterministic — the same contact always falls into the same bucket — so they are ideal for consistent samples across runs.

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "join": "and",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_universal_search",
        "operator": "contains",
        "values": ["brittney"]
      },
      {
        "type": "attribute_condition",
        "key": "_bucket",
        "operator": "range-number",
        "values": {
          "lowerNumber": 0,
          "upperNumber": 10
        }
      }
    ]
  },
  "limit": 100,
  "offset": 0,
  "sortField": "_email",
  "sortAsc": true
}
```

## Cursor-based pagination

For large traverses, use cursor pagination instead of `offset`/`limit`. A cursor keeps a consistent snapshot of the data across requests.

### How it works

1. **First request** — run a query without a cursor. The response includes a `cursor` string.
2. **Subsequent requests** — pass the `cursor` from the previous response to continue.
3. **End of results** — when `cursor` is `null`, there are no more results.

### Cursor format

The cursor is a **base64-encoded string** containing a snapshot identifier, position information and a keep-alive setting. You do not need to parse or modify it — just pass it back as-is.

### Using cursors

```json
// First request
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "children": [
      {
        "type": "attribute_condition",
        "key": "_email",
        "operator": "exists",
        "values": []
      }
    ]
  },
  "limit": 100
}

// Response includes: { "cursor": "eyJwb2ludEluVGltZUlkIjoiLi4uIn0=", ... }

// Next request — just pass the cursor
{
  "cursor": "eyJwb2ludEluVGltZUlkIjoiLi4uIn0="
}
```

> **Note**: - The cursor expires after a period of inactivity (default: 1–5 minutes).
> - `offset` is **not compatible** with cursor pagination and is cleared when a cursor is set.
> - Cursors are **stateless** — any process holding the cursor can continue the iteration.

## Aggregations

> **Warning**: Aggregations are **disabled by default** and are only available on endpoints that explicitly support them. Check the endpoint in the [API Reference](/product-api/reference) before using them. Access requires the `PROJECT_AGGREGATIONS` scope, which is manually granted by Instasent and reserved for trusted partners.

Aggregations analyse and summarise the filtered contacts. They use the standard format with configuration inside `params`; the `@` prefix references an attribute and is auto-resolved.

### Terms aggregation

Group contacts by unique values:

```json
{
  "version": "0.0.1",
  "limit": 0,
  "offset": 0,
  "aggregations": {
    "mi-agregacion": {
      "type": "terms",
      "params": {
        "field": "@_country_code",
        "size": 3
      },
      "aggs": {
        "mi-otra-agregacion": {
          "type": "terms",
          "params": {
            "field": "@_email"
          }
        }
      }
    }
  }
}
```

### Terms with "other" bucket

```json
{
  "version": "0.0.1",
  "limit": 0,
  "offset": 0,
  "aggregations": {
    "topCountries": {
      "type": "terms",
      "params": {
        "field": "@_country_code",
        "size": 5,
        "other_bucket": true,
        "other_bucket_label": "other"
      }
    }
  }
}
```

Returns the top 5 countries plus an "other" bucket containing the count of everything else.

### Nested terms with "other" buckets

Enable "other" buckets at multiple levels:

```json
{
  "version": "0.0.1",
  "limit": 0,
  "offset": 0,
  "aggregations": {
    "mainCategories": {
      "type": "terms",
      "params": {
        "field": "@_product_category",
        "size": 3,
        "other_bucket": true,
        "other_bucket_label": "other_categories"
      },
      "aggs": {
        "subCategories": {
          "type": "terms",
          "params": {
            "field": "@_product_subcategory",
            "size": 2,
            "other_bucket": true,
            "other_bucket_label": "other_subcategories"
          }
        }
      }
    }
  }
}
```

### Range aggregations

```json
{
  "version": "0.0.1",
  "limit": 0,
  "offset": 0,
  "aggregations": {
    "contactsByBucketRange": {
      "type": "range",
      "params": {
        "field": "@_bucket",
        "ranges": [
          { "from": 0,  "to": 33 },
          { "from": 33, "to": 66 },
          { "from": 66, "to": 100 }
        ]
      }
    }
  }
}
```

### Getting tags

List all system and client tags:

```json
// POST /project/my-project-1/audience/search

{
  "version": "0.0.1",
  "limit": 0,
  "aggregations": {
    "system_tags": {
      "type": "terms",
      "params": { "field": "@_tags", "size": 100 }
    },
    "client_tags": {
      "type": "terms",
      "params": { "field": "@_client_tags", "size": 100 }
    }
  }
}
```

### Nested aggregations

Aggregate on nested fields (like events):

```json
{
  "version": "0.0.1",
  "limit": 0,
  "offset": 0,
  "sortField": "_email",
  "sortAsc": true,
  "aggregations": {
    "mi-agregacion": {
      "type": "nested",
      "params": { "path": "events" },
      "aggs": {
        "tipo-de-eventos": {
          "type": "terms",
          "params": { "field": "events.type" }
        }
      }
    }
  }
}
```

## Native event attributes

When filtering by events, these native event attributes are available:

| Attribute                | Meaning                        |
| ------------------------ | ------------------------------ |
| `received-at`            | When the event was received.   |
| `created-at`             | When the event was created.    |
| `event-source`           | Source of the event.           |
| `event-type`             | Type of event.                 |
| `audience-id`            | ID of the audience contact.    |
| `audience-ids`           | Multiple audience contact IDs. |
| `audience-ds-ids`        | Audience datasource IDs.       |
| `audience-categories`    | Audience categories.           |
| `audience-target-groups` | Audience target groups.        |
| `ds-contact-id`          | Datasource contact ID.         |
| `ds-id`                  | Datasource ID.                 |
| `ds-event-id`            | Datasource event ID.           |

## Operators reference

Every condition node has the shape:

```json
{
  "type": "attribute_condition",
  "key": "<attribute key>",
  "operator": "<operator name>",
  "values": "<values — shape depends on the operator>"
}
```

The subsections below describe, for each operator, the exact shape of `values` and any behavioural notes. About `key`:

- Native attributes have UIDs prefixed with `_` (`_email`, `_phone_mobile`, `_country_code`, `_client_tags`, `_is_subscribed_sms`, `_date_birthday`, `_date_imported`, `_bucket`, `_geopoint`, …). They are defined by Instasent and always available.
- Custom attributes use the UID configured by the project (no leading `_`).
- On `event_condition`, `key` is an event attribute — either a [native event attribute](#native-event-attributes) or a custom event parameter defined by the project.
- On `segment_condition`, `key` is the segment identifier (slug), not an attribute UID.

Conventions used below:

- **Negation.** Every operator with a `-not` suffix takes the same `values` as its positive counterpart and inverts the match. A missing attribute is *not matched* by the positive operator and *is matched* by the negated one — i.e. negation is "not (positive match)", which includes contacts where the attribute is absent.
- **Multi-value attributes + `&&` prefix.** For array-valued attributes (e.g. `_client_tags`), string and numeric "list" operators default to OR semantics across the provided values. Passing `"&&"` as the first element of `values` switches to AND — *all* values must be present. `"||"` (the default) is also accepted explicitly.
- **Condition type.** Most operators work on both `attribute_condition` and `event_condition`. Exceptions: `in-segment` / `in-segment-not` are valid only on `segment_condition`, and `range-date-dayversary` is valid only on `event_condition` against `created-at`.

### Generic

These work regardless of the attribute's data type.

#### `match-all`

Matches every contact. Used internally; children of a group with this operator are collapsed away. `values` is ignored.

#### `match-none`

Matches no contacts. Used internally for invalid or empty queries. `values` is ignored.

#### `exists`

The attribute is present on the contact (non-null, and non-empty for array attributes).

```json
{ "type": "attribute_condition", "key": "_email", "operator": "exists", "values": [] }
```

#### `exists-not`

The attribute is missing or null. Same `values` shape as `exists`.

### Boolean

#### `matches-bool`

Exact boolean match. `values` must be an array with exactly one element: `[true]` or `[false]`.

```json
{ "type": "attribute_condition", "key": "_is_subscribed_sms", "operator": "matches-bool", "values": [true] }
```

> **Note**: There is no `matches-bool-not`. Use `matches-bool` with the opposite boolean, or wrap an `exists-not` inside a negated group.

### String

Apply to attribute data types `STRING`, `KEYWORD` and `TEXT`. All string operators accept an array of strings; the first element may optionally be `"&&"` or `"||"` to set the combination mode for multi-value attributes (default `||` / OR).

| Operator             | Behaviour                   | Case             | Min length |
| -------------------- | --------------------------- | ---------------- | ---------- |
| `contains`           | Substring match (`*value*`) | Case-insensitive | 2 chars    |
| `contains-not`       | Inverse of `contains`       | Case-insensitive | 2 chars    |
| `startswith`         | Prefix match (`value*`)     | Case-insensitive | 1 char     |
| `startswith-not`     | Inverse of `startswith`     | Case-insensitive | 1 char     |
| `endswith`           | Suffix match (`*value`)     | Case-insensitive | 1 char     |
| `endswith-not`       | Inverse of `endswith`       | Case-insensitive | 1 char     |
| `matches-string`     | Exact value match           | Case-sensitive   | 1 char     |
| `matches-string-not` | Inverse of `matches-string` | Case-sensitive   | 1 char     |

Max value length for `contains` / `contains-not`: 128 chars.

OR across multiple values (default):

```json
{ "type": "attribute_condition", "key": "_email", "operator": "contains", "values": ["yahoo", "hotmail"] }
```

AND across multiple values (for multi-value attributes such as `_client_tags`):

```json
{
  "type": "attribute_condition",
  "key": "_client_tags",
  "operator": "matches-string",
  "values": ["&&", "tag-5", "tag-csv-2"]
}
```

The example above matches contacts that carry **both** `tag-5` and `tag-csv-2`. Without `"&&"` it would match contacts that carry either of them.

### Numeric

Apply to data types `INT` and `DECIMAL`.

#### `matches-number` / `matches-number-not`

Exact match against one or more numbers. Same `&&` / `||` semantics as the string operators.

```json
{ "type": "attribute_condition", "key": "_bucket", "operator": "matches-number", "values": [10, 25, 50] }
```

#### `range-number` / `range-number-not`

Range query. `values` is an **object**, not an array:

```json
{
  "type": "attribute_condition",
  "key": "_bucket",
  "operator": "range-number",
  "values": {
    "lowerNumber": 0,
    "upperNumber": 40,
    "lowerExcludeEquals": false,
    "upperExcludeEquals": false
  }
}
```

| Field                | Type           | Default | Meaning                                                                  |
| -------------------- | -------------- | ------- | ------------------------------------------------------------------------ |
| `lowerNumber`        | number \| null | `null`  | Lower bound. `null` = unbounded below.                                   |
| `upperNumber`        | number \| null | `null`  | Upper bound. `null` = unbounded above.                                   |
| `lowerExcludeEquals` | bool           | `false` | If `true`, lower bound is exclusive (`gt`); otherwise inclusive (`gte`). |
| `upperExcludeEquals` | bool           | `false` | If `true`, upper bound is exclusive (`lt`); otherwise inclusive (`lte`). |

Both bounds `null` on `range-number` matches everything (and nothing on `range-number-not`).

### Date

Apply to data type `DATE`. All date operators honour the project timezone.

#### `matches-date`

Exact day match. Internally expanded to `[00:00:00, 23:59:59]` in the project timezone.

```json
{
  "type": "attribute_condition",
  "key": "_date_birthday",
  "operator": "matches-date",
  "values": { "date": "1990-05-14" }
}
```

| Field  | Type                  | Notes                  |
| ------ | --------------------- | ---------------------- |
| `date` | string (`YYYY-MM-DD`) | Required. Exactly one. |

#### `range-date` / `range-date-not`

Absolute date range.

```json
{
  "type": "attribute_condition",
  "key": "_date_imported",
  "operator": "range-date",
  "values": {
    "lowerDate": "2026-01-01",
    "upperDate": "2026-03-31",
    "lowerExcludeEquals": false,
    "upperExcludeEquals": false,
    "lowerRounding": true,
    "upperRounding": true
  }
}
```

| Field                | Type                      | Default | Meaning                                                           |
| -------------------- | ------------------------- | ------- | ----------------------------------------------------------------- |
| `lowerDate`          | ISO date/datetime \| null | `null`  | Lower bound. `null` = unbounded below.                            |
| `upperDate`          | ISO date/datetime \| null | `null`  | Upper bound. `null` = unbounded above.                            |
| `lowerExcludeEquals` | bool                      | `false` | Exclude equality on lower bound (`gt` vs `gte`).                  |
| `upperExcludeEquals` | bool                      | `false` | Exclude equality on upper bound (`lt` vs `lte`).                  |
| `lowerRounding`      | bool                      | `false` | If `true`, rounds the lower bound to the start of its day (`/d`). |
| `upperRounding`      | bool                      | `false` | If `true`, rounds the upper bound to the end of its day (`/d`).   |

#### `range-date-relative` / `range-date-relative-not`

Range expressed relative to *now*. Useful for "last N days", "next month", etc.

```json
{
  "type": "attribute_condition",
  "key": "_date_imported",
  "operator": "range-date-relative",
  "values": {
    "lowerOffset": -7,
    "upperOffset": 0,
    "lowerOffsetPeriod": "day",
    "upperOffsetPeriod": "day",
    "lowerExcludeEquals": false,
    "upperExcludeEquals": false,
    "lowerRounding": false,
    "upperRounding": false
  }
}
```

| Field                | Type        | Default | Meaning                                                                         |
| -------------------- | ----------- | ------- | ------------------------------------------------------------------------------- |
| `lowerOffset`        | int \| null | `null`  | Signed offset from now. Negative = past, positive = future. `null` = unbounded. |
| `upperOffset`        | int \| null | `null`  | Signed offset from now.                                                         |
| `lowerOffsetPeriod`  | enum        | `"day"` | Unit of `lowerOffset`. One of: `min`, `hour`, `day`, `week`, `month`, `year`.   |
| `upperOffsetPeriod`  | enum        | `"day"` | Unit of `upperOffset`. Same options.                                            |
| `lowerExcludeEquals` | bool        | `false` | Same semantics as `range-date`.                                                 |
| `upperExcludeEquals` | bool        | `false` | Same semantics as `range-date`.                                                 |
| `lowerRounding`      | bool        | `false` | Rounds the bound to the start of the period.                                    |
| `upperRounding`      | bool        | `false` | Rounds the bound to the end of the period.                                      |

Example — "imported in the last 30 days":

```json
{
  "lowerOffset": -30, "lowerOffsetPeriod": "day",
  "upperOffset": 0,   "upperOffsetPeriod": "day"
}
```

#### `range-date-anniversary`

Matches date anniversaries independent of year (e.g. "birthday is in the next 7 days", "anniversary falls between Jan 25 and Dec 01"). Only available on DATE attributes whose field supports the anniversary index.

```json
// Relative mode — N days before/after today's anniversary
{
  "values": {
    "mode": "relative",
    "lowerOffsetDays": 0,
    "upperOffsetDays": 7
  }
}

// Absolute mode — month-day range (format MMDD)
{
  "values": {
    "mode": "absolute",
    "lowerDate": "0125",
    "upperDate": "1201"
  }
}
```

| Field             | Type                         | Required when     | Notes                                                                                                       |
| ----------------- | ---------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------- |
| `mode`            | `"relative"` \| `"absolute"` | always            | Default `"relative"`.                                                                                       |
| `lowerOffsetDays` | int                          | `mode = relative` | Signed. Negative = before, positive = after. `gte`.                                                         |
| `upperOffsetDays` | int                          | `mode = relative` | Signed. Exclusive upper (`lt`). Default `1`.                                                                |
| `lowerDate`       | string (`MMDD`)              | `mode = absolute` | e.g. `"0125"` = Jan 25.                                                                                     |
| `upperDate`       | string (`MMDD`)              | `mode = absolute` | e.g. `"1201"` = Dec 01. Inverted ranges (e.g. `1215` → `0115`) wrap automatically across the year boundary. |

#### `range-date-dayversary`

Matches a weekday + hour-of-day window. Only valid on `event_condition` against `created-at`. Use it for recurring weekly schedules — e.g. "events on Friday evenings".

```json
{
  "values": { "lowerDay": "105", "upperDay": "523" }
}
```

| Field      | Type   | Format                                                                           | Example                        |
| ---------- | ------ | -------------------------------------------------------------------------------- | ------------------------------ |
| `lowerDay` | string | `DHH` — 1 digit weekday (`1` = Monday … `7` = Sunday) + 2 digit hour (`00`–`23`) | `"105"` = Monday 05:00, `gte`. |
| `upperDay` | string | same                                                                             | `"523"` = Friday 23:00, `lte`. |

If the lower day/hour is *greater* than the upper (e.g. Friday → Monday), the range wraps across the week boundary automatically.

#### `range-date-timeversary`

Matches a time-of-day window independent of the date. Two `mode`s, analogous to `range-date-anniversary`.

```json
// Relative mode — N minutes around the current time
{
  "values": {
    "mode": "relative",
    "lowerOffsetMinutes": -15,
    "upperOffsetMinutes": 15
  }
}

// Absolute mode — HH:MM range (format HHMM or HH:MM — colons are stripped)
{
  "values": {
    "mode": "absolute",
    "lowerTime": "0550",
    "upperTime": "2200"
  }
}
```

| Field                | Type                         | Required when     | Notes                                                   |
| -------------------- | ---------------------------- | ----------------- | ------------------------------------------------------- |
| `mode`               | `"relative"` \| `"absolute"` | always            | Default `"relative"`.                                   |
| `lowerOffsetMinutes` | int                          | `mode = relative` | Signed, `gte`.                                          |
| `upperOffsetMinutes` | int                          | `mode = relative` | Signed, `lt`. Default `1`.                              |
| `lowerTime`          | string (`HHMM`)              | `mode = absolute` | 24h. `"0550"` = 05:50.                                  |
| `upperTime`          | string (`HHMM`)              | `mode = absolute` | `"2200"` = 22:00. Inverted ranges wrap across midnight. |

### Geographic

#### `geopoint-distance`

Matches contacts within a radius of a `(longitude, latitude)` point. Only applies to geopoint attributes.

```json
{
  "type": "attribute_condition",
  "key": "_geopoint",
  "operator": "geopoint-distance",
  "values": {
    "longitude": -3.7038,
    "latitude": 40.4168,
    "distance": 25
  }
}
```

| Field       | Type    | Required | Notes                               |
| ----------- | ------- | -------- | ----------------------------------- |
| `longitude` | decimal | yes      | WGS-84 longitude.                   |
| `latitude`  | decimal | yes      | WGS-84 latitude.                    |
| `distance`  | decimal | yes      | Radius. Default unit is kilometres. |

All three fields are required; missing any raises a validation error.

### Segment

Used only on `segment_condition` nodes. The `key` is the segment identifier (slug), and `values` is ignored.

#### `in-segment`

Contact belongs to the named segment. Segments are evaluated recursively (a segment may reference other segments) with a nesting depth cap of 4 and circular-reference detection. A missing segment resolves to `match-none` — except the special `_all` segment, which resolves to `match-all`.

```json
{ "type": "segment_condition", "key": "high-value-customers", "operator": "in-segment" }
```

#### `in-segment-not`

Contact does *not* belong to the named segment. Same `key` semantics.

### `values` shape — quick reference

| Operator                                                                       | `values` shape                                                                                                                 |   |     |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | - | --- |
| `match-all`, `match-none`                                                      | ignored                                                                                                                        |   |     |
| `exists`, `exists-not`                                                         | `[]`                                                                                                                           |   |     |
| `matches-bool`                                                                 | `[true]` or `[false]`                                                                                                          |   |     |
| `contains(-not)`, `startswith(-not)`, `endswith(-not)`, `matches-string(-not)` | `string[]`, optional leading `"&&"` / \`"                                                                                      |   | "\` |
| `matches-number(-not)`                                                         | `number[]`, optional leading `"&&"` / \`"                                                                                      |   | "\` |
| `range-number(-not)`                                                           | `{ lowerNumber, upperNumber, lowerExcludeEquals?, upperExcludeEquals? }`                                                       |   |     |
| `matches-date`                                                                 | `{ date: "YYYY-MM-DD" }`                                                                                                       |   |     |
| `range-date(-not)`                                                             | `{ lowerDate, upperDate, lowerExcludeEquals?, upperExcludeEquals?, lowerRounding?, upperRounding? }`                           |   |     |
| `range-date-relative(-not)`                                                    | `{ lowerOffset, upperOffset, lowerOffsetPeriod, upperOffsetPeriod, lower/upperExcludeEquals?, lower/upperRounding? }`          |   |     |
| `range-date-anniversary`                                                       | `{ mode: "relative", lowerOffsetDays, upperOffsetDays }` or `{ mode: "absolute", lowerDate: "MMDD", upperDate: "MMDD" }`       |   |     |
| `range-date-dayversary`                                                        | `{ lowerDay: "DHH", upperDay: "DHH" }` (events only)                                                                           |   |     |
| `range-date-timeversary`                                                       | `{ mode: "relative", lowerOffsetMinutes, upperOffsetMinutes }` or `{ mode: "absolute", lowerTime: "HHMM", upperTime: "HHMM" }` |   |     |
| `geopoint-distance`                                                            | `{ longitude, latitude, distance }`                                                                                            |   |     |
| `in-segment`, `in-segment-not`                                                 | ignored (segment id goes in `key`)                                                                                             |   |     |

## What's next

- **[Audience event query filter](/product-api/audience-event-query-filter)** — the separate grammar for searching events directly.
- **[Query Filter](/further-reading/query-filter)** — the generic index-endpoint filter, used elsewhere across the platform.
- **[API Reference](/product-api/reference)** — endpoints that accept this filter (`/audience/search`, `/audience/scroll`, `/audience/segment/{uid}/scroll`, `/audience/aggregations`).
