Skip to content

Build a segment

Download .md

A segment is a saved filter: “everyone tagged vip who has not opted out of texts”. Unlike a contact list, 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 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.

Terminal window
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 } ] } }'
{ "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.

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.

fieldopvalue
tageq, ne, in, not_in, contains, is_set, is_not_settext, 1 to 64 characters (spaces around it are dropped)
name, companythe sametext, 1 to 200 characters
channeleq, ne, in, not_in, is_set, is_not_setsms, voice, whatsapp, email, messenger, instagram, line, telegram, slack, discord or kakao
opted_out.<channel>eq, ne, is_set, is_not_settrue or false
birth_montheq, ne, in, not_in, is_set, is_not_seta whole number, 1 to 12
attributes.<key>eq, ne, in, not_in, contains, is_set, is_not_settext, 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 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.

Terminal window
curl "https://api.omniocto.com/v1/segments/sg-0000000001/contacts?limit=50" \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY"
{ "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.

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.

Pass the segment’s id as the campaign’s audience.segment:

Terminal window
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. Opt-outs are checked again when it sends, whatever the filter says.

The /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.