Segment your audience
Goal: build a dynamic segment filter, preview who it matches before saving it, save it, re-check it later, and see how many of its contacts a campaign will actually reach.
Prerequisites: contacts already in your account (see Import contacts); an API key with the contacts:read and contacts:write scopes; Contacts & audiences for how segments differ from static lists.
A segment is a saved filter over contact attributes. Its membership is computed when it is used, not stored — previewing shows what the filter matches right now.
Steps
1. Preview a filter before saving
curl -X POST https://emails.routeon.io/api/v1/segments/preview \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"filter": {
"op": "and",
"conditions": [
{ "field": "status", "operator": "eq", "value": "subscribed" },
{ "field": "custom.plan", "operator": "in", "value": ["pro", "enterprise"] }
]
}
}'
The call answers with a 2xx status; its payload:
{
"count": 214,
"sample": [
{ "id": "<CONTACT_ID>", "email": "jane@example.com", "first_name": "Jane", "last_name": "Doe", "status": "subscribed" }
],
"capped": false
}
countis the exact number of matching contacts.sampleholds up to 10 matching contacts withid,email,first_name,last_nameandstatus(nocustomvalues).cappedistruewhencountis above 10,000 — a hint that the audience is large, not an estimate;countstays exact.
How to write a filter. A filter is a tree: a group is { "op": "and" | "or", "conditions": [...] }, and each condition is either another group or a leaf { "field", "operator", "value" }. A filter with a single condition can also be just that leaf, without a group.
| Part | Allowed values |
|---|---|
field |
email, status, first_name, last_name, or custom.<key> for a custom field (key of letters, digits, _, -) |
operator |
eq, neq, in, nin (with an array value), contains (case-insensitive substring), exists (value true or false), gt, lt |
value |
A string, or an array of strings for in/nin |
Values are compared as text, including custom values stored as numbers or dates, so gt and lt order values alphabetically ("10" sorts before "9"). Prefer eq, in and contains for exact matching. An unknown field or operator is refused with 422.
The preview counts every contact that matches, whatever its status and whether or not its address is suppressed. Add a status condition, as above, to see only subscribed contacts — and see step 5 for the audience a campaign really sends to.
2. Save the segment
curl -X POST https://emails.routeon.io/api/v1/segments \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"name": "Pro and enterprise subscribers",
"filter": {
"op": "and",
"conditions": [
{ "field": "status", "operator": "eq", "value": "subscribed" },
{ "field": "custom.plan", "operator": "in", "value": ["pro", "enterprise"] }
]
}
}'
Expected response (201 Created):
{
"status": "success",
"http_code": 201,
"payload": {
"id": "<SEGMENT_ID>",
"tenant_id": "<TENANT_ID>",
"name": "Pro and enterprise subscribers",
"kind": "dynamic",
"filter": {
"op": "and",
"conditions": [
{ "field": "status", "operator": "eq", "value": "subscribed" },
{ "field": "custom.plan", "operator": "in", "value": ["pro", "enterprise"] }
]
},
"deleted_at": null,
"created_at": "2026-09-28T09:00:00Z"
},
"meta": {}
}
kind is dynamic for a segment and static for a list created with POST /lists. The segment object’s member_count is not a live figure — use preview for counts.
3. Re-preview a saved segment
curl https://emails.routeon.io/api/v1/segments/<SEGMENT_ID>/preview \
-H "Authorization: Bearer <API_KEY>"
200 OK returns the same count, sample and capped shape as step 1, evaluated against your contacts as they are now.
4. Update the name or the filter
curl -X PATCH https://emails.routeon.io/api/v1/segments/<SEGMENT_ID> \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"filter": {
"op": "and",
"conditions": [
{ "field": "status", "operator": "eq", "value": "subscribed" },
{ "field": "custom.plan", "operator": "eq", "value": "enterprise" }
]
}
}'
200 OK returns the updated segment. Send at least one of name or filter.
5. See the audience a campaign will reach
A campaign sends only to contacts that are subscribed and whose address is not suppressed, and subtracts any exclude audiences. Its real size therefore comes from the campaign, not from the segment preview. After creating a campaign that includes the segment (see Send your first campaign):
curl -X POST https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>/preview \
-H "Authorization: Bearer <API_KEY>"
payload.recipient_count is the number of recipients the campaign would send to now, with up to 5 addresses in sample_emails.
6. List or delete segments
curl https://emails.routeon.io/api/v1/segments \
-H "Authorization: Bearer <API_KEY>"
curl -X DELETE https://emails.routeon.io/api/v1/segments/<SEGMENT_ID> \
-H "Authorization: Bearer <API_KEY>"
GET /lists lists static lists in the same shape. Deleting a segment only removes the saved filter; no contact is changed.
Verify
GET /segments/{id}/preview returns the count and sample you expect, and POST /campaigns/{id}/preview shows how many of them a campaign reaches. Pass the segment to a campaign as { "kind": "segment", "id": "<SEGMENT_ID>" } in audience.include or audience.exclude.
Common failures
| Symptom | Cause | Fix |
|---|---|---|
422 on POST /segments/preview or POST /segments |
Unknown field, unknown operator, or a custom key with characters other than letters, digits, _, - |
Check meta.validation_error and fix the filter |
Preview fails or count is unexpected for in/nin, gt/lt |
value has the wrong shape (not an array for in/nin), or non-string values |
Send strings, and arrays of strings for in/nin |
gt/lt on a number matches the wrong contacts |
Values are compared as text | Use eq/in, or store values that sort correctly as text (e.g. zero-padded) |
Campaign reaches fewer contacts than the preview count |
The preview includes unsubscribed, bounced, complained and suppressed contacts | Use POST /campaigns/{id}/preview for the real audience |
422 on PATCH /segments/{id} |
Neither name nor filter supplied |
Send at least one |
404 on a segment endpoint |
The id does not exist, belongs to another account, or was deleted | Check with GET /segments |