Guides

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

fieldMatches
statusThe contact's subscription status
emailThe contact's email address
created_atWhen 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:

opMeaningvalue must be
eqequalsstring, number, or boolean
neqdoes not equalstring, number, or boolean
containssubstring matchstring
gt, gtegreater than, or equalnumber
lt, lteless than, or equalnumber
existsthe field is presentignored
inthe value is one of a listnon-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

CodeMeaning
filter_not_objectthe filter or a condition was not a JSON object
unknown_fieldnot a known field, and no attributes. prefix
unknown_opnot one of the nine operators
bad_valuewrong value type for the operator, or match was not all
bad_attribute_keythe attribute key is empty, too long, or has illegal characters
too_many_conditionsmore than 25 conditions
invalid_namethe 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.

On this page