Audience targeting
Point a campaign draft at the right contacts: segment UIDs, inline audience filters, exclusions and the consent policy. Includes the filter grammar the audience field expects and how to count the reach before creating anything.
The audience object decides who a campaign reaches. It holds two lists — include and exclude — and both accept the same two kinds of entry: a segment UID, or an inline filter written in the same grammar the segment endpoints return. You can mix them in one list, which is what makes it possible to say "everyone in the VIP segment plus everyone in Spain with a mobile number, minus the people we messaged this week" in a single call.
Two kinds of entry
- A segment UID — a string. Targets an existing segment by identity: the campaign keeps the link, so the dashboard shows the audience under the segment's own name (VIP customers), not as an anonymous filter. The special UID
_allis the whole audience. - An inline filter — an object. A filter sent with the request, stored on the campaign as a custom audience labelled by its position, exactly as if it had been built with the filter builder in the panel.
Prefer a segment UID whenever the audience already exists as a segment: it keeps the campaign readable in the dashboard, and a marketer opening the draft recognises what they are looking at. Reach for an inline filter when the criteria are computed by your own system and would not be worth persisting as a segment.
{
"audience": {
"include": ["vip-customers"],
"exclude": ["recently-messaged"]
}
}Defaults, and how the two lists combine
- Omit
audienceentirely, or send an emptyinclude, and the campaign targets everyone:includedefaults to["_all"]. - A contact matching any entry of
includeis in. excludeis applied afterwards: a contact matching any entry of it is removed, whichever include put them there.- Repeated segment UIDs within a list are collapsed, so building a list from several code paths cannot double-count.
Targeting segments
Segment UIDs are the slugs the segment endpoints return, not database ids. List them first if you are building a picker:
curl "$BASE/segment" \
-H "Authorization: Bearer $INSTASENT_TOKEN"Then target one, several, or all of them:
{
"audience": {
"include": ["vip-customers", "newsletter-subscribers"],
"exclude": ["employees"]
}
}A UID that does not exist in the project is refused with unknown-segment rather than quietly resolving to nobody — a typo in a segment name would otherwise produce a campaign that looks fine and reaches no one.
Inline filters (custom audiences)
An inline filter is an object with a root group. Send it in place of a UID:
{
"audience": {
"include": [
{
"root": {
"type": "group",
"children": [
{
"type": "attribute_condition",
"key": "_country_code",
"operator": "matches-string",
"values": ["ES"]
}
]
}
}
]
}
}Three rules govern them:
- At least one condition. A root group with no children would be stored as a "custom audience" that silently matches everyone. Targeting everyone is legitimate; doing it by accident under a label that says otherwise is not, so an empty filter is rejected with
invalid-audience-filter. - Validated before the draft is stored. Both the structure and the contents are checked up front: an unknown operator or an attribute that does not exist in the project is refused at creation, with the reason in the message, instead of failing later when someone opens the draft and the audience is first counted.
- No segment identity. A filter you supply is a custom audience by definition; any segment metadata sent along with it is discarded.
Writing the filter
The grammar is the Audience Query Filter (AQF), the same one behind /audience/search and every dynamic segment. The full reference documents every condition type and operator; what follows is the part that matters when you are writing one by hand for a campaign.
A filter is a root group holding children, combined with AND unless the group says otherwise:
{
"root": {
"type": "group",
"join": "and",
"children": [
{
"type": "attribute_condition",
"key": "_country_code",
"operator": "matches-string",
"values": ["ES"]
},
{
"type": "attribute_condition",
"key": "_phone_mobile",
"operator": "exists",
"values": []
}
]
}
}The operators you will reach for most in a campaign audience:
| Operator | Matches | values |
|---|---|---|
matches-string | Exact value, case-sensitive | ["ES"] |
matches-number | Exact number | [3] |
contains / startswith / endswith | Substring, prefix, suffix — case-insensitive | ["gmail"] |
exists / exists-not | The attribute is present / absent | [] |
matches-bool | Boolean equality | [true] |
range-number | Numeric range | { "lowerNumber": 100, "upperNumber": null } |
range-date | Absolute date range | { "lowerDate": "2026-01-01", "upperDate": "2026-03-31" } |
range-date-relative | Range relative to now | { "lowerOffset": -30, "lowerOffsetPeriod": "day" } |
in-segment / in-segment-not | Segment membership, on a segment_condition | ignored — the UID goes in key |
Every operator with a -not suffix inverts its positive counterpart. Conditions can also target events (group_event + event_condition) — "bought in the last 30 days" is an event condition, not an attribute one. The AQF reference has the exact shapes.
Mixing segments and filters
Both lists are heterogeneous, so a segment and a filter can sit side by side. This targets a loyalty segment plus every Spanish contact with a mobile number, minus the people already messaged this week:
{
"channel": "sms",
"title": "Flash sale for Spanish contacts",
"audience": {
"include": [
"loyalty-members",
{
"root": {
"type": "group",
"children": [
{
"type": "attribute_condition",
"key": "_country_code",
"operator": "matches-string",
"values": ["ES"]
},
{
"type": "attribute_condition",
"key": "_phone_mobile",
"operator": "exists",
"values": []
}
]
}
}
],
"exclude": ["recently-messaged"]
},
"message": {
"text": "Solo hoy: 30% de descuento en toda la tienda.",
"allowUnicode": true
}
}In the dashboard this draft shows two included audiences — the segment under its name, the filter as a custom audience — and one exclusion.
Consent policy
compliance decides which contacts the audience is allowed to reach once consent is taken into account. It is a top-level field, not part of audience, and defaults to the project's own policy when omitted.
| Value | Reaches |
|---|---|
basic | Every active contact, ignoring marketing preferences. Maximum reach. |
opt-out | Everyone except contacts who opted out of this channel. |
opt-in | Only contacts with explicit consent. |
The three policies also differ in what happens when a recipient opts out, which is a product decision rather than an API one — the full model is in Consent policies.
The project's own policy is readable before you draft anything: GET /project/{project} returns it as generalConfig.channelSms.defaultCompliancePolicy (and channelRcs, channelWhatsapp, with channelDefaults covering the channels that set none). It is always resolved, never null. When compliance is omitted the draft takes that value and stores it on the campaign, so the campaign carries its policy explicitly rather than inheriting it later.
The same three policies are available as a filter, which is how you count the audience under a policy before creating anything: see Counting who can receive.
Count the reach before you create
The audience of a campaign is only known once it is resolved, and a draft is a poor place to discover that a filter matches eleven people. Count first:
Count the audience contacts matching a filter.
curl -X POST "$BASE/audience/count" \
-H "Authorization: Bearer $INSTASENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"version": "0.0.1",
"root": {
"type": "group",
"children": [
{
"type": "attribute_condition",
"key": "_country_code",
"operator": "matches-string",
"values": ["ES"]
}
]
},
"filterCompliance": { "sms": "opt-out" }
}'It takes the same filter shape as an inline audience entry, so you can count exactly what you are about to send, and it is a cheap read compared to creating and deleting drafts.
filterCompliance is what makes the total a sending figure: name the channel the campaign will use and the policy it will carry (the one you are about to send as compliance, or the project's own if you omit it) and the count applies the same consent and reach rules the campaign applies, so it matches the audience figure the dashboard shows for that draft. Drop the key and you get the raw audience instead: every contact matching the conditions, including those with no mobile number and those the policy would exclude. The key, its policies and its channels are documented in Counting who can receive.
Errors
errorCode | Cause |
|---|---|
unknown-segment | A UID does not exist in this project, or include/exclude is not a list of UIDs and filters. |
invalid-audience-filter | The filter is not valid AQF, has no conditions, or names an operator or attribute that does not exist. The message names the offending part. |