Skip to main content

Audience Filters

Audience filters narrow a notification's recipients by device and subscriber attributes — the tenant-defined metadata your SDKs report (OS version, locale, SDK version, custom properties). Filters are applied at the source when the audience is resolved, so a filtered send never fans out to non-matching devices.

Where filters go

device_filters and subscriber_filters are top-level siblings of target in the create-notification request. device_filters matches against each device's attributes; subscriber_filters matches against the owning subscriber's attributes. When both are present they are ANDed — a device is included only when its own attributes satisfy device_filters and its subscriber's attributes satisfy subscriber_filters.

{
"target": { "type": "all" },
"device_filters": {
"locale": { "$in": ["en-US", "en-GB"] }
},
"subscriber_filters": {
"tier": { "$eq": "premium" }
}
}

Operators

OperatorMeaning
$eqequals (a bare value is an implicit $eq)
$nenot equal (the key must be present)
$gt, $gte, $lt, $lteordered comparison
$invalue is in the array
$ninvalue is not in the array (the key must be present)
$existstrue requires the key; false requires its absence

All predicates across keys are ANDed.

Missing values

For comparison operators ($eq, $ne, $gt, $gte, $lt, $lte, $in, $nin), a device whose attribute is absent is excluded. Use {"$exists": false} to match devices that lack a key, and {"$exists": true} to require it.

Scalar attributes

String attributes compare by value; ordering operators compare lexicographically.

{ "device_filters": { "locale": { "$ne": "fr-FR" } } }

Version attributes

Versions are compared numerically, segment by segment17.10 is greater than 17.2, which plain string comparison gets wrong. To make this unambiguous, versions are stored and queried as a structured object:

{ "major": 17, "minor": 2, "patch": 1 }

minor and patch default to 0 when omitted.

The known version attributes — os_version, sdk_version, and app_version — are parsed into this structured form automatically at ingestion, so a device that reports "17.2.1" is stored as {"major":17,"minor":2,"patch":1}.

Filter on them with object operands:

{
"device_filters": {
"os_version": { "$gte": { "major": 17, "minor": 0, "patch": 0 } }
}
}
{
"device_filters": {
"sdk_version": { "$in": [ { "major": 3, "minor": 2, "patch": 1 },
{ "major": 4, "minor": 0, "patch": 0 } ] }
}
}
note

Version-typed keys require object operands. A string operand on os_version/sdk_version/app_version (e.g. {"$gte": "17.0"}) is rejected with a 422 VALIDATION_ERROR. Comparing a version operand against an attribute that is not a structured version object excludes that device.

Custom version fields

You can treat your own attribute as a version. Report it in the structured {major, minor, patch} shape from your SDK (custom keys are not auto-parsed from strings — only the three known keys are), then filter on it the same way:

{
"device_filters": {
"firmware_version": { "$gte": { "major": 2, "minor": 5, "patch": 0 } }
}
}
Indexing

Equality, set, and existence predicates ($eq/$in/$exists) are index-accelerated for any key. Version range predicates ($gt/$gte/$lt/$lte) are index-accelerated only for the three known keys (os_version, sdk_version, app_version), which have dedicated expression indexes. Range filters on a custom version key still return correct results but scan more rows; add a matching expression index if you rely on them at scale.

Complete example

Send to premium subscribers on iOS 17+ in English locales:

{
"target": { "type": "all" },
"channel_ids": ["chan-ios"],
"content": { "title": "New for you", "body": "Tap to explore" },
"device_filters": {
"locale": { "$in": ["en-US", "en-GB"] },
"os_version": { "$gte": { "major": 17, "minor": 0, "patch": 0 } }
},
"subscriber_filters": {
"tier": { "$eq": "premium" }
}
}