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.
Quickstart
Contacts with email containing "yahoo"
{
"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
{
"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 agroupcontaining 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:truefor ascending,falsefor 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 at the bottom for the full schemas, values shapes and behavioural notes.
Common patterns
Filter by email
{
"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).
{
"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
{
"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:
{
"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:
{
"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:
{
"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:
{
"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.
{
"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.
{
"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
- First request — run a query without a cursor. The response includes a
cursorstring. - Subsequent requests — pass the
cursorfrom the previous response to continue. - End of results — when
cursorisnull, 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
// 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="
}Aggregations
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:
{
"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
{
"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:
{
"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
{
"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:
// 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):
{
"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:
{
"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,keyis an event attribute — either a native event attribute or a custom event parameter defined by the project. - On
segment_condition,keyis the segment identifier (slug), not an attribute UID.
Conventions used below:
- Negation. Every operator with a
-notsuffix takes the samevaluesas 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 ofvaluesswitches to AND — all values must be present."||"(the default) is also accepted explicitly. - Condition type. Most operators work on both
attribute_conditionandevent_condition. Exceptions:in-segment/in-segment-notare valid only onsegment_condition, andrange-date-dayversaryis valid only onevent_conditionagainstcreated-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).
{ "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].
{ "type": "attribute_condition", "key": "_is_subscribed_sms", "operator": "matches-bool", "values": [true] }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):
{ "type": "attribute_condition", "key": "_email", "operator": "contains", "values": ["yahoo", "hotmail"] }AND across multiple values (for multi-value attributes such as _client_tags):
{
"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.
{ "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:
{
"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.
{
"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.
{
"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.
{
"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":
{
"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.
// 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".
{
"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 modes, analogous to range-date-anniversary.
// 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.
{
"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.
{ "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 — the separate grammar for searching events directly.
- Query Filter — the generic index-endpoint filter, used elsewhere across the platform.
- API Reference — endpoints that accept this filter (
/audience/search,/audience/scroll,/audience/segment/{uid}/scroll,/audience/aggregations).