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
| Operator | Meaning |
|---|---|
$eq | equals (a bare value is an implicit $eq) |
$ne | not equal (the key must be present) |
$gt, $gte, $lt, $lte | ordered comparison |
$in | value is in the array |
$nin | value is not in the array (the key must be present) |
$exists | true 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 segment — 17.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 } ] }
}
}
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 } }
}
}
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" }
}
}