# Build a segment

> Save a filter over your contacts with /v1/segments, see who is in it, change it, and use it as a campaign's audience.

A segment is a saved filter: "everyone tagged `vip` who has not opted out
of texts". Unlike a [contact list](/platform-api/contacts/#lists), nobody is
added to it by hand. It is re-read every time it is used, so contacts join
and leave as their tags, attributes and opt-outs change.

**You need:** an [API key](/getting-started/api-keys/) created by an owner or
admin of the workspace to create, change or delete a segment (any key reads
them). Every field: [Senders and lookups reference](/platform-api/reference/lookups/).

## Create a segment

```bash
curl -X POST https://api.omniocto.com/v1/segments \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "name": "VIPs on SMS", "filter": { "all": [
        { "field": "tag", "op": "eq", "value": "vip" },
        { "field": "opted_out.sms", "op": "eq", "value": false } ] } }'
```

```json
{ "id": "sg-0000000001", "object": "segment", "uuid": "00000000-0000-4000-8000-000000000051",
  "name": "VIPs on SMS", "created_at": "2026-10-29T09:00:00.000Z", "contacts": 412,
  "filter": { "all": [ { "field": "tag", "op": "eq", "value": "vip" },
                       { "field": "opted_out.sms", "op": "eq", "value": false } ] } }
```

The answer is `201`, and `contacts` is how many people the filter matches
right now. Send an `Idempotency-Key` with every create: the same request
with the same key answers the first segment again, never a second one.

## The filter

A contact is in the segment when it matches **every** condition in `all`
and, when `any` is given, **at least one** condition in `any`. Deleted
contacts never match. A filter needs at least one condition: to reach
everyone, use a contact list.

| `field` | `op` | `value` |
|---|---|---|
| `tag` | `eq`, `ne`, `in`, `not_in`, `contains`, `is_set`, `is_not_set` | text, 1 to 64 characters (spaces around it are dropped) |
| `name`, `company` | the same | text, 1 to 200 characters |
| `channel` | `eq`, `ne`, `in`, `not_in`, `is_set`, `is_not_set` | `sms`, `voice`, `whatsapp`, `email`, `messenger`, `instagram`, `line`, `telegram`, `slack`, `discord` or `kakao` |
| `opted_out.<channel>` | `eq`, `ne`, `is_set`, `is_not_set` | `true` or `false` |
| `birth_month` | `eq`, `ne`, `in`, `not_in`, `is_set`, `is_not_set` | a whole number, 1 to 12 |
| `attributes.<key>` | `eq`, `ne`, `in`, `not_in`, `contains`, `is_set`, `is_not_set` | text, a number, or `true` / `false` (lists: text or numbers), compared as text |

- `in` and `not_in` take a list of 1 to 100 values; `is_set` and
  `is_not_set` take no `value`; the others take one. `contains` ignores
  case (`eq` does not) and matches the text literally: `%` and `_` are not
  wildcards.
- `all` and `any` take up to 20 conditions each.
- `<key>` is a custom contact field key, as
  [`GET /v1/contact-fields`](/platform-api/contacts/#custom-fields) lists them.
- `opted_out.sms` with `eq false` means "has not opted out of texts", the
  same people as `is_not_set`. Use whichever reads better.
- A contact with no value: on `name`, `company`, `birth_month` and
  `attributes.<key>`, `ne` matches it and `not_in` does not. To include
  contacts with no `company`, add `{ "field": "company", "op": "is_not_set" }`
  to an `any` group. On `tag` and `channel`, both match a contact with an
  empty tag list or no channels.

A filter with a problem is `400 invalid_filter`. `param` names the first
problem (`filter.all.1.value`), the message says what that field takes, and
`issues` lists every problem at once, so you can fix them in one go.
Nothing is saved.

## See who is in it

```bash
curl "https://api.omniocto.com/v1/segments/sg-0000000001/contacts?limit=50" \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY"
```

```json
{ "object": "list", "data": [
    { "id": "ct-00000000a1", "object": "contact", "uuid": "00000000-0000-4000-8000-000000000031",
      "name": "Ana", "phone": "+15555550100", "email": null,
      "created_at": "2026-10-18T09:00:00.000Z" } ],
  "has_more": true, "next_cursor": "YzF8..." }
```

Newest contacts first, up to 200 a page; pass `next_cursor` as `after` for
the next page. Membership is live, so someone can join or leave between two
pages. A cursor belongs to the filter it was read with: once the filter
is different it is `invalid_cursor`, and you start again. A very broad filter
over a large workspace can answer `503 segment_too_slow`: narrow it.

## Change or delete it

`PATCH /v1/segments/{id}` takes a new `name`, a new `filter`, or both. A
`filter` replaces the whole filter, so read the segment first
(`GET /v1/segments/{id}` answers its `filter`), change it, and send it back.
A segment saved in the dashboard reads in the same names; a condition saved
there in a form `/v1` does not take keeps its value as stored, and a PATCH has to
replace it.

`DELETE /v1/segments/{id}` deletes it. Its contacts stay.

A campaign that is queued, running or paused reads its segment live, so
while one uses the segment, a new `filter` and a delete are both
`409 segment_in_use`, with the campaigns in `campaigns`. Nothing is changed,
the name included. A rename alone always works. A draft does not block
either one: after a filter change, preview the draft again; after a
delete, starting it is refused until it names another audience.

## Use it in a campaign

Pass the segment's `id` as the campaign's `audience.segment`:

```bash
curl -X POST https://api.omniocto.com/v1/campaigns \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "channel": "sms", "name": "October VIP offer", "start": "draft", "from": "+16285550100",
        "audience": { "segment": "sg-0000000001" },
        "content": { "text": "Hi {{first_name}}, 20% off for VIPs this week." },
        "variables": { "first_name": "{{contact.first_name|there}}" } }'
```

The campaign is a draft until you [preview and start it](/platform-api/campaigns/).
Opt-outs are checked again when it sends, whatever the filter says.

## From Claude Code

The [`/mcp` API tools](/mcp/api-tools/) `create_segment`, `update_segment`,
`delete_segment` and `list_segment_contacts` do the same. `delete_segment`
first answers what it would delete; it deletes only when called again with
`confirm: true`.