Skip to Content
Segments

Segments

A segment is a saved filter over one audience. Segments are live: membership is computed at read time and never materialized, so a segment is always current and there is nothing to refresh.

Use one to target a campaign, or as a ?segment_id= filter when listing contacts.

The Segment object

{ "id": "seg_456m9x4v0dz1", "object": "segment", "name": "Pro-plan customers", "filter": { "match": "all", "conditions": [ { "field": "plan", "op": "equals", "value": "pro" }, { "field": "company", "op": "is_set" } ] }, "created_at": "2026-05-01T10:00:00.000Z", "updated_at": "2026-05-01T10:00:00.000Z" }

The filter contract

filter is the whole contract, and it is stable.

KeyValues
match"all" or "any".
conditions1 to 10 conditions.

Each condition is { field, op, value? }.

field is email, first_name, last_name, or any custom attribute key.

op is one of:

OperatorTakes a valueNotes
equalsYes
not_equalsYes
containsYesSubstring.
not_containsYes
is_setNoHas any non-empty value.
is_not_setNo
greater_thanYesNumeric comparison.
less_thanYesNumeric comparison.

is_set and is_not_set take no value. greater_than and less_than need a numeric one.

New operators may be added over time. That is additive and non-breaking: nothing is ever removed.

A filter rejected on write is 400 invalid_request, and param names the exact path that failed, such as filter.conditions.0.value. The separate invalid_filter code belongs to the read side: it means a stored filter no longer parses, so listing that segment’s members fails rather than quietly matching everyone.

value is always a JSON string, including for greater_than and less_than. Send "value": "5", never "value": 5: a bare number fails validation with 400 invalid_request. The comparison itself is numeric, so "5" and "40" order correctly, and a row whose stored value is not a number simply does not match.

A missing attribute is treated as an empty value, so plan not_equals pro includes contacts with no plan at all. That is usually what a human means by it, and it is worth knowing before you use a segment to target a send.

List segments

GET/v1/audiences/{audience_id}/segments

Create a segment

POST/v1/audiences/{audience_id}/segments
FieldTypeRequired
namestringYes
filterobjectYes
curl -X POST https://go.day3.app/api/v1/audiences/aud_123/segments \ -H "Authorization: Bearer $DAY3_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Pro customers", "filter": { "match": "all", "conditions": [ { "field": "plan", "op": "equals", "value": "pro" }, { "field": "company", "op": "is_set" } ] } }'

Retrieve a segment

GET/v1/audiences/{audience_id}/segments/{segment_id}

Update a segment

PATCH/v1/audiences/{audience_id}/segments/{segment_id}

name, filter, or both. A filter you send replaces the previous one whole rather than merging into it.

Delete a segment

DELETE/v1/audiences/{audience_id}/segments/{segment_id}

Deletes the saved filter. Contacts are untouched, because a segment never owned them.

List the members of a segment

GET/v1/audiences/{audience_id}/segments/{segment_id}/contacts

Current matches, evaluated at request time, with standard pagination. Returns Contact objects.

curl "https://go.day3.app/api/v1/audiences/aud_123/segments/seg_456/contacts?limit=100" \ -H "Authorization: Bearer $DAY3_API_KEY"

Equivalent to listing contacts with ?segment_id=seg_456. Use whichever reads better in your code.

Last updated on