Build a segment
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.
Create a segment
Section titled “Create a segment”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.
The filter
Section titled “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 |
inandnot_intake a list of 1 to 100 values;is_setandis_not_settake novalue; the others take one.containsignores case (eqdoes not) and matches the text literally:%and_are not wildcards.allandanytake up to 20 conditions each.<key>is a custom contact field key, asGET /v1/contact-fieldslists them.opted_out.smswitheq falsemeans “has not opted out of texts”, the same people asis_not_set. Use whichever reads better.- A contact with no value: on
name,company,birth_monthandattributes.<key>,nematches it andnot_indoes not. To include contacts with nocompany, add{ "field": "company", "op": "is_not_set" }to ananygroup. Ontagandchannel, 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
Section titled “See who is in it”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.
Change or delete it
Section titled “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
Section titled “Use it in a campaign”Pass the segment’s id as the campaign’s audience.segment:
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.
From Claude Code
Section titled “From Claude Code”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.