Segments
Filter contacts by status, email, creation time, or custom attributes — the filter grammar, the 25-condition ceiling, and how to preview a segment before you send.
A segment is a saved filter over your contacts. Broadcasts send to them, automations branch on them, and you can count or preview one before committing to a send.
Filter shape
{
"match": "all",
"conditions": [
{ "field": "status", "op": "eq", "value": "subscribed" },
{ "field": "attributes.plan", "op": "in", "value": ["pro", "team"] }
]
}`match` accepts only `all`
Conditions are always ANDed. There is no any or or semantics today — a
filter with "match": "any" is rejected outright. To express an
either/or over one field, use the in operator with the values you want;
across different fields, keep two segments.
match may be omitted entirely; all is the only valid value either way.
Fields
field | Matches |
|---|---|
status | The contact's subscription status |
email | The contact's email address |
created_at | When the contact was created |
attributes.<key> | A custom attribute on the contact |
Custom attributes require the attributes. prefix. A bare plan is
rejected with unknown_field, while attributes.plan reads the plan key
from the contact's attributes.
The key after the prefix must be 1–64 characters of A–Z, a–z, 0–9, _,
. or -. Anything else is rejected with bad_attribute_key.
Operators
Each operator accepts a specific value type, and the wrong type is rejected
with bad_value rather than coerced:
op | Meaning | value must be |
|---|---|---|
eq | equals | string, number, or boolean |
neq | does not equal | string, number, or boolean |
contains | substring match | string |
gt, gte | greater than, or equal | number |
lt, lte | less than, or equal | number |
exists | the field is present | ignored |
in | the value is one of a list | non-empty array of strings/numbers |
`created_at` is epoch milliseconds, so comparisons take a number
The comparison operators require a number, and created_at is stored as
epoch milliseconds. {"field":"created_at","op":"gt","value":"2026-01-01"}
is rejected with bad_value — pass 1767225600000 instead.
Limits
A filter holds at most 25 conditions. More returns
too_many_conditions.
Preview before you send
Three ways to see what a filter catches, which is worth doing before any broadcast:
# A filter you haven't saved yet
curl https://api.emitd.com/v1/segments/preview \
-H "Authorization: Bearer $EMITD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "filter": { "match": "all", "conditions": [
{ "field": "status", "op": "eq", "value": "subscribed" } ] } }'
# A saved segment's members, and just its size
curl https://api.emitd.com/v1/segments/$ID/preview -H "Authorization: Bearer $EMITD_API_KEY"
curl https://api.emitd.com/v1/segments/$ID/count -H "Authorization: Bearer $EMITD_API_KEY"count is the cheap one — reach for it when all you need is "how many
people would this hit".
Passing user input into a filter
Every condition value and every attribute key is bound as a SQL parameter, never interpolated into the query text. A filter is safe to build from user-supplied values; you still want to validate them for your own reasons, but injection is not among them.
Errors
| Code | Meaning |
|---|---|
filter_not_object | the filter or a condition was not a JSON object |
unknown_field | not a known field, and no attributes. prefix |
unknown_op | not one of the nine operators |
bad_value | wrong value type for the operator, or match was not all |
bad_attribute_key | the attribute key is empty, too long, or has illegal characters |
too_many_conditions | more than 25 conditions |
invalid_name | the segment's name was missing or blank |
Segments vs audiences
A segment is a filter, evaluated when you use it, so it tracks your contact data as it changes. An audience is a static list holding exactly whoever you put in it. Reach for a segment when membership is a rule, and an audience when it is a decision.
Full schemas: Segments API.