Segments
Build live, nested audience rules over contact fields, list membership and engagement
Segments
A segment is a saved rule, not a saved list of people. It stores a definition and re-resolves every time it is counted, previewed, or sent to — including at the moment a scheduled campaign actually fires, not when it was scheduled. A contact who matches tomorrow is included tomorrow, with no intervention.
All endpoints require an Authorization: Bearer YOUR_API_KEY header.
The segment object
{
"id": "sg_abc123",
"name": "Singapore · women · 25–35",
"description": null,
"definition": { "op": "and", "children": [ /* … */ ] },
"condition_count": 3,
"created_at": "2026-08-09T10:30:00Z",
"updated_at": "2026-08-09T10:30:00Z"
}The definition grammar
A definition is a nested boolean tree. Groups have an op (and / or)
and children; leaves have a kind.
{
"op": "and",
"children": [
{ "op": "or", "children": [
{ "kind": "attribute", "field": "city", "operator": "eq", "value": "Singapore" },
{ "kind": "attribute", "field": "city", "operator": "eq", "value": "Kuala Lumpur" }
]},
{ "kind": "attribute", "field": "date_of_birth", "operator": "age_between", "value": [25, 35] },
{ "kind": "attribute", "field": "interested_in", "operator": "contains_any", "value": ["women"] },
{ "kind": "list", "operator": "in_list", "value": "ls_newsletter" },
{ "kind": "engagement", "operator": "opened_in_last_days", "value": 30 },
{ "kind": "contact", "field": "status", "operator": "eq", "value": "subscribed" }
]
}Limits: 25 conditions, nested up to 3 levels. Exceeding either returns
422 naming the limit. An empty group is an error — neither "matches everyone"
nor "matches nobody" is safe to guess.
Leaf kinds
kind | Targets | Operators |
|---|---|---|
attribute | A registered contact field by field key | Per the field's type |
contact | Core columns: status, created_at, email_domain | eq, neq, in, not_in, before, after, between, in_last_days |
list | List membership | in_list, not_in_list |
engagement | Opens, clicks and receipts | opened_in_last_days, clicked_in_last_days, not_opened_in_last_days, opened_campaign, clicked_campaign, received_campaign |
Age
age_between is an operator on a date field and takes [min, max] inclusive:
{ "kind": "attribute", "field": "date_of_birth", "operator": "age_between", "value": [20, 35] }It compiles to a birthdate window evaluated against the current date, so nobody has to re-run anything on a birthday.
Two semantics worth knowing
Negative attribute operators require the attribute to be present.
neq, not_in and not_contains match only contacts whose attribute is set
and different. A contact with the attribute missing does not match.
"Not X" means we know, and it isn't X — never we don't know. If 3% of your
contacts have no gender recorded, gender neq male will not sweep them in.
Engagement negatives are the opposite. not_opened_in_last_days does
match a contact with no events at all, because there the absence of a row is the
fact: they did not open.
Opens are honest opens — proxy/prefetch opens are excluded, matching the campaign report.
Create a segment
POST /v1/segments| Parameter | Type | Required |
|---|---|---|
name | string | Yes |
description | string | null | No |
definition | object | Yes |
curl -X POST https://api.envloped.com/v1/segments \
-H "Authorization: Bearer en_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "Singapore 25-35",
"definition": {
"op": "and",
"children": [
{ "kind": "attribute", "field": "city", "operator": "eq", "value": "Singapore" },
{ "kind": "attribute", "field": "date_of_birth", "operator": "age_between", "value": [25, 35] }
]
}
}'The definition is normalised as it is validated — what is stored is canonical (trimmed strings, coerced numbers, de-duplicated arrays), so read it back rather than assuming it round-trips byte-for-byte.
Errors
| Status | error | Meaning |
|---|---|---|
422 | unknown_field | A leaf references a field key that is not registered, or has been archived. |
422 | invalid_segment | The tree is malformed, exceeds a limit, or uses an operator the field's type does not support. |
409 | duplicate_segment | A live segment with that name already exists. |
Both 422s include an errors array listing every problem found, not just the
first.
Other endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /v1/segments | All live segments. |
GET | /v1/segments/:id | One segment. |
PATCH | /v1/segments/:id | Rename, re-describe, or replace the definition. |
DELETE | /v1/segments/:id | Soft delete. Campaigns that already sent keep their records. |
GET | /v1/segments/:id/count | How many contacts match right now. |
GET | /v1/segments/:id/preview?limit=25 | Count plus a sample of real contacts. |
POST | /v1/segments/preview | Evaluate an unsaved definition — the endpoint the visual builder uses. |
curl "https://api.envloped.com/v1/segments/sg_abc123/count" \
-H "Authorization: Bearer en_abc123..."
# { "id": "sg_abc123", "count": 4210 }What the count does and does not include
count answers exactly one question: how many contacts match this
definition. It does not subtract unsubscribes, suppressions, or contacts that
are not eligible to be mailed.
The number that matters before a send is the campaign-level eligible figure
from GET /v1/campaigns/:id/audience/preview, which layers
those gates on top of this same rule. Conflating the two would make the segment
count lie about what the segment is.