# Lists & Segments

The audience API exposes the same **Lists**, **Segments**, and custom **attributes** your dashboard shows. There are three resources:

- **Lists** — static audiences with explicit membership. A contact is in a List because it was added, and stays until it's removed.
- **Segments** — dynamic audiences defined by a **SendQL predicate**. Membership is computed from each contact's attributes and engagement, and is re-evaluated continuously — you never add members by hand.
- **Attributes** — the registry of custom attribute definitions (name, type, optional enum values). This is the schema that Segment predicates reference as `attr.<name>`.

**Segments** are **read-only** in the Public API — every endpoint is a `GET`, with one exception: `POST /v1/segments/preview` is a non-mutating dry-run that evaluates a candidate predicate without saving anything. Segments are created and maintained from the dashboard or as code in a connected git repository (see [Git-backed definitions](#git-backed-lists-segments-and-attributes) below).

**Lists** are different: List *definitions* are writable through the API with the `api.lists.manage` scope — create, edit, and delete — while **membership** stays on the contacts surface (`lists` on [`/v1/contacts`](/api-reference/contacts)). See [Managing List definitions](#managing-list-definitions).

**Attributes** are also writable. Beyond the read endpoints, you can create, edit, delete, and dry-run-preview attribute definitions with the `api.attributes.manage` scope — see [Managing attribute definitions](#managing-attribute-definitions). Editing the schema can affect the Segments that reference it, so every write reports its [mutation class](#mutation-classes-and-impact).


  List and Segment members are returned by `contact_id` **and** `email`. To answer "is this one contact a member?" without paging the whole audience, use the membership-check endpoint — it returns `200` with `is_member: true|false` and only `404`s when the List or Segment itself doesn't exist.


## How it maps to the dashboard

| Dashboard | API |
|---|---|
| **Audience → Lists** | [`GET /v1/lists`](/api-reference/endpoints/lists/list-lists) |
| Create a List | [`POST /v1/lists`](/api-reference/endpoints/lists/create-list) |
| A single List's detail page | [`GET /v1/lists/{id}`](/api-reference/endpoints/lists/get-list) · [`GET /v1/lists/{id}/members`](/api-reference/endpoints/lists/list-list-members) |
| Edit / delete a List | [`PUT /v1/lists/{id}`](/api-reference/endpoints/lists/update-list) · [`DELETE /v1/lists/{id}`](/api-reference/endpoints/lists/delete-list) |
| **Audience → Segments** | [`GET /v1/segments`](/api-reference/endpoints/segments/list-segments) |
| A single Segment's detail page | [`GET /v1/segments/{id}`](/api-reference/endpoints/segments/get-segment) · [`GET /v1/segments/{id}/members`](/api-reference/endpoints/segments/list-segment-members) |
| The Segment predicate editor's live preview | [`POST /v1/segments/preview`](/api-reference/endpoints/segments/preview-segment) |
| Custom attribute definitions | [`GET /v1/attributes`](/api-reference/endpoints/attributes/list-attributes) |

## Scopes

Five scopes gate this surface, each granted by the matching dashboard permission:

| Scope | Grants | PII |
|---|---|---|
| `api.lists.view` | `/v1/lists`, `/v1/lists/{id}`, `/v1/lists/{id}/members`, and the membership check | **Yes** — returns member contact ids and emails. Grant deliberately. |
| `api.lists.manage` | Create (`POST /v1/lists`), edit (`PUT /v1/lists/{id}`), and delete (`DELETE /v1/lists/{id}`) List definitions | **No** — definition-only (name, key, description). Membership add/remove is a separate write on the contacts surface (`api.contacts.manage`). |
| `api.segments.view` | `/v1/segments`, `/v1/segments/{id}`, `/v1/segments/{id}/members`, the membership check, and `/v1/segments/preview` | **Yes** — returns member contact ids/emails plus the predicate itself. |
| `api.attributes.view` | `/v1/attributes`, `/v1/attributes/{id}` | **No** — definitions only (name, type, enum values). The schema, not recipient data. |
| `api.attributes.manage` | Create (`POST /v1/attributes`), edit (`PUT /v1/attributes/{id}`), delete (`DELETE /v1/attributes/{id}`), and preview (`POST /v1/attributes/{id}/preview`) attribute definitions | **No** — operates on the schema. The disruptive-edit impact payload returns affected Segment ids/names and a drop-out count, not recipients. |

`api.attributes.view` is the safe one to hand to an integration that only needs to know what fields exist (for example, to render a Segment-builder UI) without being able to read any recipient. `api.attributes.manage` and `api.lists.manage` are write scopes — grant them only to integrations that own your attribute schema or your List definitions respectively, since a disruptive attribute edit can flip dependent Segments into a warning state, and a List delete leaves its membership log orphaned (see below).

## Pagination

List and Segment **collections** (`/v1/lists`, `/v1/segments`, `/v1/attributes`) are bounded per org and returned newest-first. **Member** collections (`/v1/lists/{id}/members`, `/v1/segments/{id}/members`) can be large and are [cursor-paginated](/api-reference/pagination) — follow `pagination.next_cursor` until it's `null`. Every collection response carries `data` plus a `pagination` object.

## Lists

A List is a static membership set. Each member carries an `added_at` timestamp — when the contact was put on the List.



`key` is an org-unique slug that stays stable across renames — prefer it over `name` when you reference a List from your own systems. `source` (`api` · `ui` · `import` · `topic_export`) records how the List was created — `topic_export` marks a snapshot List exported from a subscription topic. `origin` (`managed` · `git`) and `status` (`active` · `archived`) follow the same [git-backed](#git-backed-lists-segments-and-attributes) provenance model as Segments and attributes; a git-backed List also carries `source_path`.

## Managing List definitions

With the `api.lists.manage` scope you can author static-List *definitions* over the API — membership itself is managed separately, via `lists` on the [contacts surface](/api-reference/contacts#inline-static-list-membership):

| Operation | Endpoint | Notes |
|---|---|---|
| Create | [`POST /v1/lists`](/api-reference/endpoints/lists/create-list) | Body: `name` (required), `key` (optional, defaults to a slug of `name`; immutable afterwards), `description` (optional). `201` with the new [`V1List`](#lists). A `key` already in use — including one owned by a git-backed List — is `409 conflict`. |
| Edit | [`PUT /v1/lists/{id}`](/api-reference/endpoints/lists/update-list) | Body: `name`, `description`. `{id}` resolves as a UUID or, failing that, the List's `key`. `key` and `source` are immutable — a `key` in the body is ignored. |
| Delete | [`DELETE /v1/lists/{id}`](/api-reference/endpoints/lists/delete-list) | `204` on success, idempotent-ish in that the membership log is left in place (harmless once the definition is gone). |


  A List whose `origin` is `git` can't be edited or deleted through the API — `PUT`/`DELETE /v1/lists/{id}` return `409 conflict`, the same "can't mutate what git owns" rule Segments and attributes follow. Change it in the connected repository (the `lists` key in your manifest) instead; SendOps reconciles it on push, and removing the key from the repo archives (never deletes) the List. See [Git-backed Lists, Segments, and attributes](#git-backed-lists-segments-and-attributes).


## Segments

A Segment is defined by a **SendQL predicate** that resolves to a boolean per contact. The predicate references custom attributes as `attr.<name>` (the names come from [`/v1/attributes`](/api-reference/endpoints/attributes/list-attributes)) plus engagement events and subscription state. A Segment's response includes:

| Field | Meaning |
|---|---|
| `source` | The SendQL expression that defines membership. |
| `profile_version` | The version of the SendQL function/field profile the source was compiled against. |
| `eval_class` | How membership is kept current: `incremental` (updated on events), `sweep` (recomputed on a schedule), or `both`. |
| `status` | `active`, `paused`, `invalid` (the predicate no longer compiles), or `archived` (a git-backed Segment whose source file was removed). |
| `eval_warning` | A standing "evaluation disrupted" warning, present when a [disruptive attribute edit](#managing-attribute-definitions) has left the predicate referencing a changed or removed attribute. The Segment keeps evaluating best-effort until its predicate is re-saved. Absent when there's no warning. |
| `eval_warning_at` | When the standing `eval_warning` was raised; absent when none. |
| `member_count` | Cached size, from the last evaluation. |
| `last_evaluated_at` | Absent until the Segment has been evaluated at least once. |

Segment members carry `entered_at` — when the contact most recently entered the Segment (membership can churn as attributes change).


  `member_count` and the members list reflect the **last evaluation**, not this instant. A Segment that's `paused` or `invalid` stops being re-evaluated, so its membership freezes at the last good run. Check `status` and `last_evaluated_at` before treating a count as live.


### Previewing a predicate

`POST /v1/segments/preview` evaluates a candidate predicate against your current contacts **without saving anything** — the same call the dashboard's predicate editor makes. It returns the match `count`, the inferred `eval_class`, and a small `sample` of matching contacts. An invalid predicate returns `422 validation_failed` with the compile error, so you can validate before persisting a Segment elsewhere.





## Attributes

The attribute registry is the schema Segment predicates compile against. Each definition has a lowercase `name` (referenced in SendQL as `attr.<name>`), a `type` (`string` · `number` · `bool` · `datetime` · `enum`), and, for `enum`, the allowed `enum_values`.



## Managing attribute definitions

With the `api.attributes.manage` scope you can author the attribute registry over the API — the same schema your Segments compile against:

| Operation | Endpoint | Returns |
|---|---|---|
| Create | [`POST /v1/attributes`](/api-reference/endpoints/attributes/create-attribute) | `201` with the new [`V1Attribute`](#attributes). |
| Edit | [`PUT /v1/attributes/{id}`](/api-reference/endpoints/attributes/update-attribute) | The updated definition, its `mutation_class`, and (for a disruptive edit) the committed `impact`. |
| Delete | [`DELETE /v1/attributes/{id}`](/api-reference/endpoints/attributes/delete-attribute) | The `mutation_class` (always `disruptive`) and `impact`. |
| Preview | [`POST /v1/attributes/{id}/preview`](/api-reference/endpoints/attributes/preview-attribute) | The projected `mutation_class` and `impact` — **no writes**. |

The request body for create, edit, and preview is the same shape: `name` (lowercase identifier referenced as `attr.<name>`), `type` (`string` · `number` · `bool` · `datetime` · `enum`), `enum_values` (required when `type` is `enum`, rejected otherwise), and an optional `description`.


  An edit **always applies** — it is never blocked. What changes is the *impact* on the Segments that reference the attribute, reported as the `mutation_class`. Preview that impact first with `POST /v1/attributes/{id}/preview` (it performs no writes), then commit the same body with `PUT`.


### Mutation classes and impact

Every edit, delete, and preview is classified by how it affects dependent Segment predicates:

| `mutation_class` | Meaning |
|---|---|
| `free` | No effect on any predicate — a description change, an enum widening, or moving between `string` and `enum` (which compile to the same cast). |
| `safe_but_stale` | An `enum` value was removed. Predicates stay valid but may compare against a value that can no longer be written. |
| `disruptive` | A rename or a cast-class change (e.g. `string` → `number`), or **any delete**. Segments that reference the attribute keep evaluating best-effort but carry a standing [`eval_warning`](#segments) until their predicate is re-saved. |

A `disruptive` edit (and every delete) returns an `impact` block:

| Field | Meaning |
|---|---|
| `affected_segments[]` | Each Segment referencing the attribute — `id`, `name`, and `typecheck_ok` (whether its predicate still type-checks under the change), plus the `warning` the edit raises. |
| `projected_dropout` | Org-wide count of contacts that drop out of the affected Segments under the change. `0` unless the change is disruptive. |






  An attribute whose `origin` is `git` can't be edited or deleted through the API — these calls return `409 conflict`. Change it in the connected repository instead; SendOps reconciles it on push. See [Git-backed Lists, Segments, and attributes](#git-backed-lists-segments-and-attributes).


## Git-backed Lists, Segments, and attributes

Lists, Segments, and attribute definitions can all be **managed as code** in a connected GitHub repository (a List via the manifest's `lists` key, a Segment via its own file, an attribute via the attribute schema). When a definition originates from git, its response carries:

| Field | Meaning |
|---|---|
| `origin` | `managed` (created in the dashboard / via the session API) or `git` (defined in the connected repo). |
| `source_path` | The repo-relative file the definition is defined in (git-backed only). |
| `last_synced_at` | When SendOps last reconciled it from git (git-backed only). Segments and attributes only — a List reports git-backed provenance via `origin`/`source_path`/`status` without a `last_synced_at` field. |

Edit git-backed definitions in the repository — SendOps validates and reconciles them on push, and a removed source file moves the definition to `archived` (a List's key, a Segment's or attribute's source file). These fields are simply absent for dashboard-managed definitions.

## Worked example: export a Segment's members

Resolve the Segment by its stable `key`, then page its members to the end.




  Reading a List or Segment tells you who's a member right now. It does not enforce opt-outs — to respect subscription state at send time, send through SES `SendEmail` with `ListManagementOptions`. See [Contacts & Topics](/api-reference/contacts).