Segments
A segment is everyone matching its filter, plus the contacts added to it by hand. Membership is recomputed on every read, so a contact that gains a matching property joins immediately. There is no rebuild that can fall behind.
A filter with no conditions contributes nobody, so such a segment holds only its hand-added members. This is how you build a fixed list.
The segment object
{
"id": "…",
"name": "Pro accounts",
"slug": "pro_accounts",
"description": "Paying customers who still accept mail",
"filter": { "all": [ ] },
"public_name": "Product news",
"show_in_preferences": true,
"created_at": "…",
"updated_at": "…"
}Create and update
POST /v1/segments
GET /v1/segments
GET /v1/segments/{id}
PATCH /v1/segments/{id}
DELETE /v1/segments/{id}| Field | Required | Notes |
|---|---|---|
name | yes | Up to 200 characters. |
slug | yes | Up to 64. Unique per organization. |
description | no | A blank string is stored as null. |
filter | no | The tree below. Omit for a segment that holds only hand-added members. |
public_name | no | What recipients see on the preference page. Blank means unset. |
show_in_preferences | no | Default false. When true, members can opt out of this segment on the preference page. |
The filter tree
A node is either a group — {"all": [...]} or {"any": [...]} — or a condition — {"field": …, "op": …, "value": …}. Groups nest.
{
"all": [
{ "field": "unsubscribed", "op": "eq", "value": false },
{ "any": [
{ "field": "property:plan", "op": "eq", "value": "pro" },
{ "field": "email", "op": "ends_with", "value": "@acme.com" }
]}
]
}Empty groups
Inside a filter that has conditions, an empty all matches everyone and an empty any matches no one. A filter with no conditions anywhere matches nobody, and the segment holds only its hand-added members. To target every contact, add a condition such as email contains @.
Fields and operators
There are three kinds of field, and each accepts a fixed set of operators. A combination outside the table is refused with notifyzr.segment.error.invalid_filter, and the message names the problem.
| Field | Operators | Value |
|---|---|---|
email | eq ne contains starts_with ends_with | A non-empty string |
unsubscribed | eq | true or false. Reads the opt-out from all email only. |
property:<key>, text | eq ne contains starts_with ends_with set not_set | A non-empty string |
property:<key>, number | eq ne gt gte lt lte set not_set | A number |
property:<key>, boolean | eq set not_set | true or false |
property:<key>, date | eq before after set not_set | "YYYY-MM-DD" |
set and not_set ask whether the key is present at all, and take novalue.
A property: field must name a declared property definition — an undeclared key is refused, because the declaration is what says which operators are legal.
Limits
A filter nests at most 5 levels deep and holds at most 100 conditions.
How values are compared
Three details that decide what happens to imperfect data:
- Numbers and booleans compare as their own types, never by casting. One malformed stored value cannot break the segment — that row simply drops out of the filter. This is the deliberate answer to "what happens after a property is retyped".
- Dates compare as text, which is safe because they are stored
YYYY-MM-DDand ISO dates sort lexicographically. - Nothing from the tree is ever put into SQL as text — not values, not property keys. Every one is a bound parameter.
Reading the members
GET /v1/segments/{id}/contactsPaginated (page, per_page, default 25, max 100). Each row is a contact plus two flags: manual says it was added by hand, and segment_unsubscribed says it opted out of this segment's broadcasts. An opt-out does not end membership.
curl "http://api.localhost/v1/segments/$SEGMENT_ID/contacts?page=1&per_page=100" \
-H "X-API-Key: $NOTIFYZR_API_KEY"Because membership is live, two calls a minute apart can legitimately return different totals. Do not treat a count as stable.
Adding and removing members by hand
POST /v1/segments/{id}/members
DELETE /v1/segments/{id}/members/{contactId}curl -X POST "http://api.localhost/v1/segments/$SEGMENT_ID/members" \
-H "X-API-Key: $NOTIFYZR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_ids": ["3f2504e0-4f89-41d3-9a0c-0305e82c3301"]}'contact_ids takes up to 1,000 ids. If any id is not a contact of your organization, nothing is added and the call returns 422 notifyzr.segment.error.unknown_contacts. Contacts already added are left alone, and the response reports added and already_members.
Removing a member takes away the manual membership only. A contact that also matches the filter stays in the segment. A contact that was not added by hand returns 404 notifyzr.segment.error.member_not_found.
Opt-outs
A recipient can opt out of one segment on the preference page. That suppresses broadcasts to that segment only; the contact stays a member and still receives everything else.
Deleting
Deleting removes the definition, its hand-added members and its opt-outs. No contact is touched. A segment a broadcast points at cannot be deleted while that broadcast exists.