# 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.

**Language:** en
**Audience:** developer
**TLDR:** audience.include and audience.exclude take segment UIDs (strings) and inline Audience Query Filters (objects), mixed freely; include defaults to ["_all"], the whole audience. An inline filter needs a root group with at least one condition, uses named operators and values as an ARRAY — there is no eq operator. Count the reach first with POST /audience/count, adding filterCompliance (basic, opt-out, opt-in) to count under the policy the campaign will carry.
**Search keywords:** audience, targeting, segment, segments, recipients, who receives, filter, custom audience, exclude, suppression, consent policy, compliance, filterCompliance, opt-in, opt-out, basic, count contacts, audience count, reach, default policy
**Related pages:** /developers/product-api/campaigns/creating-a-draft, /developers/product-api/campaigns/overview, /developers/product-api/audience/query-filter
**Docs index (every page):** https://docs.instasent.com/llms.txt
**This zone's index:** https://docs.instasent.com/developers/product-api/llms-full.txt
**This page:** https://docs.instasent.com/developers/product-api/campaigns/audience/ (HTML) · https://docs.instasent.com/developers/product-api/campaigns/audience.md (Markdown)

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 `_all` is 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.

```json
{
  "audience": {
    "include": ["vip-customers"],
    "exclude": ["recently-messaged"]
  }
}
```

## Defaults, and how the two lists combine

- Omit `audience` entirely, or send an empty `include`, and the campaign targets **everyone**: `include` defaults to `["_all"]`.
- A contact matching **any** entry of `include` is in.
- `exclude` is 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.

> **Note**: To target the whole audience, use the `_all` segment rather than a filter with no conditions. An empty filter is rejected — see [Inline filters](#inline-filters-custom-audiences) below.

## Targeting segments

Segment UIDs are the slugs the segment endpoints return, not database ids. List them first if you are building a picker:

```bash
curl "$BASE/segment" \
  -H "Authorization: Bearer $INSTASENT_TOKEN"
```

Then target one, several, or all of them:

```json
{
  "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:

```json
{
  "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](/developers/product-api/audience/query-filter) 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:

```json
{
  "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": []
      }
    ]
  }
}
```

> **Warning**: **Operators are named, and `values` is always an array.** There is no `eq`, no `=` and no singular `value` field. The equality operator is `matches-string` (or `matches-number` for numbers), and its argument goes in `values` as a list — `"values": ["ES"]`, never `"value": "ES"`. This is the single most common mistake when writing a filter by hand.

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](/developers/product-api/audience/query-filter) has the exact shapes.

> **Tip**: **The shortcut: read a segment back and adapt it.** `GET /v1/project/{project}/segment/{uid}` returns a segment's own filter, under `queryFilter`, in exactly the shape this field expects. Build the audience once in the dashboard's filter builder, read it, and use it as the template for the filters your integration generates. Faster than writing one from scratch, and guaranteed to be valid.

## 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:

```json
{
  "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](/platform/en/campaigns/compliance-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](/developers/product-api/audience/query-filter#counting-who-can-receive-filtercompliance).

## 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:

[`POST /v1/project/{project}/audience/count` - Count the audience contacts matching a filter.](/developers/product-api/reference)

```bash
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](/developers/product-api/audience/query-filter#counting-who-can-receive-filtercompliance).

> **Note**: One thing the count does not know is which countries the campaign's sender covers, so a campaign can deliver to fewer contacts than the policy count when the sender does not reach every country in the audience. Let the dashboard's own estimate, computed against the chosen channel and sender, be the figure a human confirms.

## 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. |

## What's next

- [Audience query filter](/developers/product-api/audience/query-filter) - The full grammar: every condition type, operator and value shape.
- [Message content](/developers/product-api/campaigns/message) - What the audience you just defined is going to receive.
- [Creating a draft](/developers/product-api/campaigns/creating-a-draft) - Back to the endpoint contract, limits and error codes.

---

This is one page of the Instasent documentation. For the complete machine-readable index of every guide and API reference, fetch https://docs.instasent.com/llms.txt — start there for full context.
