Audience query filter
Search and segment the unified audience with a structured JSON filter. Combine attribute, event and segment conditions with AND/OR logic, count who can receive on a channel under a consent policy, paginate with cursors and run aggregations.
The Audience Query Filter (AQF) 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.
One grammar, three things to do with it: search it on /audience/search, count it on /audience/count, and save it on /project/{project}/segment, where the filter you just composed becomes a named segment the project keeps — one a campaign can target and another filter can reference through in-segment. See Saving a filter as a 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"]
}
]
}
}Counting who can receive: filterCompliance
A plain count answers how many contacts match these conditions. It does not answer how many of them can actually be messaged, which is the figure that decides whether a campaign is worth preparing. filterCompliance turns the first question into the second: name a channel and a consent policy, and the filter applies the same consent and reach rules a campaign applies when it estimates its audience and again when it sends, so the count and the send cannot disagree.
It is a top-level key shaped as an object of channel to policy, applied on top of root rather than expressed as conditions inside it:
{
"version": "0.0.1",
"filterCompliance": {
"sms": "opt-out"
}
}Posted to /project/{project}/audience/count, that body returns how many contacts an SMS campaign under the opt-out policy reaches. Posted to /project/{project}/audience/search, it returns those contacts. Combine it with a root to scope the question to part of the audience:
{
"version": "0.0.1",
"root": {
"type": "group",
"join": "and",
"children": [
{
"type": "segment_condition",
"key": "vip-customers",
"operator": "in-segment"
}
]
},
"filterCompliance": {
"sms": "opt-in"
},
"filterCountryCodes": ["ES", "PT"]
}The three policies
Consent lives in two attributes per channel: _is_subscribed_<channel>, which says whether the contact is still reachable on the channel at all, and _accepts_marketing_<channel>, which records their marketing preference. A policy is a rule over those two values.
| Policy | _is_subscribed_<channel> | _accepts_marketing_<channel> | Counts |
|---|---|---|---|
basic | must not be false | ignored | every contact not blocked on the channel, whatever their marketing preference |
opt-out | must not be false | must not be false | everyone except contacts who refused marketing; no stated preference still counts |
opt-in | must not be false | must be true | only contacts with explicit marketing consent |
The middle row is the one that surprises callers: a contact imported without consent data has no stated preference, and opt-out (the common policy) reaches them. Only opt-in requires an explicit true. How a contact comes to hold each state, and what a STOP does to it, is the customer-facing side of the same model: How consent works and Consent policies in the Platform guides.
On top of consent, every policy requires a valid mobile number (_phone_mobile): a contact with no reachable number is not a recipient under any policy. For rcs and whatsapp the filter additionally drops contacts known not to support the channel, while keeping those whose support is still unknown, which is the same criterion a campaign with SMS fallback uses. And rcs reads the SMS consent attributes: a contact who opted out of SMS is opted out of RCS too.
Channels, combinations and errors
- Channels:
sms,rcs,whatsapp. - Policies:
basic,opt-out,opt-in. - Several channels are AND-ed.
{"sms": "opt-out", "rcs": "opt-in"}counts the contacts that satisfy both, not the union. - Omit the key to get the raw count: no consent, no reach, just the conditions in
root.
Channel and policy are matched exactly. An unknown channel ("SMS") or an unknown policy ("optin") is rejected with a 422 Unprocessable Entity carrying type: "INVALID_COMPLIANCE_FILTER" and the offending value, instead of being ignored: a typo that silently counted the whole audience would be worse than a failed request.
Reading the project's policy
Every project carries a default consent policy per channel, and it is what a campaign draft created without an explicit compliance takes. Read it from the project resource:
curl "https://api.instasent.com/v1/project/$INSTASENT_PROJECT" \
-H "Authorization: Bearer $INSTASENT_TOKEN"The policy is generalConfig.channelSms.defaultCompliancePolicy (and channelRcs, channelWhatsapp, with channelDefaults covering the channels that set none). It is always resolved when read, never null: the channel's own value, then channelDefaults, then basic. Feed that value straight into filterCompliance to count what a campaign under the project's own policy would reach, and see Audience targeting for how a draft resolves it.
Top-level scope keys
filterCompliance belongs to a small family of keys that scope the whole query instead of describing a contact condition. They are applied on top of root, so consent, reach, country and origin never have to be expressed as conditions:
| Key | Type | Effect |
|---|---|---|
filterCompliance | object | Consent and reach for a channel, under a named policy (above). |
filterMandatoryAttributes | string[] | Attribute uids that must have a value, e.g. ["_phone_mobile"] for contacts with a valid mobile. |
filterCountryCodes | string[] | ISO 3166-1 alpha-2 codes. Keeps contacts whose resolved country matches, taking the phone's country first and _country_code after. |
filterByChannelSupport | object | Reach by device support, per channel, for channels with verification-based reachability (rcs, whatsapp): {"rcs": [true]} keeps only verified contacts, {"rcs": [true, null]} also keeps those not verified yet. Already implied by filterCompliance; set it to be stricter. |
filterDatasourceIds | string[] | Keeps contacts that came from any of these datasource ids. |
sortField / sortAsc | string / boolean | Sorting, on /audience/search only. /audience/count ignores them, as it does limit and offset. |
The id, universal-search and bucket filters are in Additional filter options above.
Saving a filter as a segment
A filter you post to /audience/search or /audience/count lives for one request. Post it with a name to /project/{project}/segment instead and the project keeps it as a named segment: a campaign can target it, another filter can reference it through an in-segment condition, and you can ask for its current size whenever you need it.
The saved queryFilter takes exactly the grammar documented on this page and is validated the same way, so compose it against /audience/count first. The request shape, the scope it needs, the cached-size semantics and the usage audit all live on the Segments page.
Save an audience filter as a named segment the project keeps.
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
- Probes: measure what the audience actually contains before you filter it: attribute fill rates, the values an attribute really holds, and which event types arrive.
- Counting and retrieval: which endpoint to post this filter to, and what each of them returns.
- Segments: keep the filter you just composed as a named segment the project holds, for campaigns and for other filters to reference.
- Audience event query filter (EQF): the separate grammar for searching events directly.
- Query Filter (QF): the generic filter for the Product API's list endpoints, unrelated to this one.
- API Reference: endpoints that accept this filter (
/audience/search,/audience/count,/audience/scroll,/audience/segment/{uid}/scroll,/audience/aggregations,/audience/coverageto probe the population it selects, and/segmentto save one).