> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message`, the default import of the v7 Node SDK. Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Audiences

> Rule-based groups that update as profiles change, addressed with audience_id.

export const Endpoint = ({method, path, name, href, children, bare}) => {
  const verb = String(method || "").toUpperCase();
  const title = verb + " " + path;
  const label = children || name || path;
  if (bare) {
    return href ? <a href={href}><code>{title}</code></a> : <code>{title}</code>;
  }
  if (!href) {
    return <span className="cx-endpoint" data-method={verb} title={title}>
        <span className="cx-endpoint-label">{label}</span>
        <span className="cx-endpoint-method">{verb}</span>
      </span>;
  }
  return <a className="cx-endpoint" data-method={verb} href={href} title={title}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">{verb}</span>
    </a>;
};

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

An audience is a group Courier maintains. You write rules, and membership updates as profiles change.

Target one on a send with `audience_id`:

```json theme={null}
{
  "to": { "audience_id": "active-business-users" }
}
```

Create or update an audience with <Endpoint method="PUT" path="/audiences/{audience_id}" name="Update an Audience" href="/docs/api-reference/audiences/update-an-audience" />, passing a `filter`. See the <Endpoint method="GET" path="/audiences/{audience_id}" name="Get an Audience" href="/docs/api-reference/audiences/get-an-audience">Audiences API</Endpoint> for the full schema.

## How it works

Each condition in a filter names a profile `path`, an `operator`, and a `value`.

### Audience operators

Console labels and their API values:

| Category  | Console                     | Code          | Use case                                       |
| --------- | --------------------------- | ------------- | ---------------------------------------------- |
| Equality  | is                          | `EQ`          | Match an exact value (a plan, role, or status) |
|           | is not                      | `NEQ`         | Exclude a value                                |
| String    | includes                    | `INCLUDES`    | Substring match (email domain, title)          |
|           | does not include            | `OMIT`        | Exclude a substring                            |
|           | starts with                 | `STARTS_WITH` | Prefix match                                   |
|           | ends with                   | `ENDS_WITH`   | Suffix match (`@acme-corp.com`)                |
| Numeric   | is greater than             | `GT`          | Above a threshold                              |
|           | is greater than or equal to | `GTE`         | At or above a limit                            |
|           | is less than                | `LT`          | Below a threshold                              |
|           | is less than or equal to    | `LTE`         | At or below a cap                              |
| Date      | is after                    | `IS_AFTER`    | After a date                                   |
|           | is before                   | `IS_BEFORE`   | Before a date                                  |
| Existence | exists                      | `EXISTS`      | The field is present                           |

### Combining conditions

A filter is a group with an `operator` and a `filters` array. Use `AND` so every condition must match, or `OR` so at least one must. A condition inside `filters` can be another group. That nests rules like "on the pro plan AND (admin OR owner)":

```json theme={null}
{
  "filter": {
    "operator": "AND",
    "filters": [
      { "operator": "EQ", "path": "profile.custom.plan", "value": "pro" },
      {
        "operator": "OR",
        "filters": [
          { "operator": "EQ", "path": "profile.custom.role", "value": "admin" },
          { "operator": "EQ", "path": "profile.custom.role", "value": "owner" }
        ]
      }
    ]
  }
}
```

### How membership works

Courier pre-computes membership instead of evaluating every rule against every profile at send time. When a profile changes, Courier updates whether that user belongs to each audience. Changing an audience's rules starts a rebuild of its membership list.

At send time, Courier reads the stored membership. A rebuild of a large audience can take many minutes. A send during the rebuild reads a partially updated list. Wait for membership to settle before a large send. Check <Endpoint method="GET" path="/audiences/{audience_id}" name="Get an Audience" href="/docs/api-reference/audiences/get-an-audience" /> and spot-check <Endpoint method="GET" path="/audiences/{audience_id}/members" name="List Audience members" href="/docs/api-reference/audiences/list-audience-members" />.

### Scoping a send to a tenant

`context.tenant_id` on a send controls branding, preferences, and template variables for that tenant. It does **not** restrict which audience members receive the message. To send only to users in one tenant, add an inline `filters` array on the recipient. Use the `MEMBER_OF` operator with `path: tenant_id`:

```json theme={null}
{
  "to": {
    "audience_id": "active-business-users",
    "filters": [
      { "operator": "MEMBER_OF", "path": "tenant_id", "value": "acme-corp" }
    ]
  },
  "context": { "tenant_id": "acme-corp" }
}
```

A user must have tenant membership to pass the filter. Set it with <Endpoint method="PUT" path="/users/{user_id}/tenants/{tenant_id}" name="Add a user to a Tenant" href="/docs/api-reference/tenant-memberships/add-a-user-to-a-tenant" />.

## Limits & behavior

* **`MEMBER_OF` is a send-time filter, not an audience rule.** It is not one of the audience comparison operators. Use it inline on the recipient to scope a send by tenant.
* **Membership is materialized, so it can lag a rule change.** A large audience rebuilds over minutes, and sends read the stored list. Wait for it to settle before a critical blast.
* **Audiences are eventual, lists are exact.** An audience member appears once the profile change propagates. A <Doc href="/docs/recipients/lists-and-audiences/lists">list</Doc> member is present the moment you add them.

## FAQ

<AccordionGroup>
  <Accordion title="Why did a user not receive an audience send right after I changed the rules?">
    Membership is pre-computed and rebuilds after a rule change. For a large audience that takes many minutes. Sends during the rebuild read a partially updated list. Wait for membership to settle before sending.
  </Accordion>

  <Accordion title="Should I use an audience or a list?">
    Use an audience when membership follows from profile data and should update itself, like all pro-plan users. Use a <Doc href="/docs/recipients/lists-and-audiences/lists">list</Doc> when you decide membership by hand.
  </Accordion>

  <Accordion title="Can I nest AND and OR in one filter?">
    Any condition in a `filters` array can be a group with its own `operator` and `filters`.
  </Accordion>
</AccordionGroup>
