Audience segments
An audience segment is a saved filter over the contact table. Unlike a contact list, the membership of a segment is dynamic — a contact is "in" the segment whenever they match the filter, evaluated at send time. So a "VIP customers" segment defined as attributes.segment eq "vip" automatically picks up new VIPs as they're tagged, without anyone having to re-add them.
Segments resolve to a where clause at send time. They power campaign audience selection alongside lists, and can be previewed without saving.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/audience-segments |
List segments |
POST |
/api/v1/audience-segments |
Create a segment |
GET |
/api/v1/audience-segments/:id |
Retrieve a segment |
PATCH |
/api/v1/audience-segments/:id |
Update a segment |
DELETE |
/api/v1/audience-segments/:id |
Delete a segment |
POST |
/api/v1/audience-segments/:id/preview |
Preview saved segment |
POST |
/api/v1/audience-segments/preview |
Preview ad-hoc filter |
All endpoints require the merchant role.
The filter shape
Every segment carries a filter document:
{
"match": "all",
"rules": [
{ "field": "email", "op": "neq", "value": null },
{ "field": "subscriptions.email", "op": "eq", "value": "subscribed" },
{ "field": "attributes.segment", "op": "in", "value": ["vip", "platinum"] }
]
}
| Field | Type | Notes |
|---|---|---|
match |
enum | all (AND across rules) or any (OR). |
rules |
Rule[] (≤20) | The conjuncts/disjuncts. |
Each rule:
| Field | Type | Notes |
|---|---|---|
field |
string (1–80) | A dotted path against the contact object. Top-level fields (email, phone, firstName) work directly; nested fields use dots (attributes.segment, subscriptions.email). |
op |
enum | One of in, not_in, eq, neq, gte, lte, gt, lt. |
value |
any | Type depends on op — in/not_in take arrays; others take scalars. |
Supported fields
Direct fields:
email,phone,firstName,lastName,source,externalRef— strings.createdAt,updatedAt,deletedAt— ISO 8601 timestamps.gte/lteaccept either ISO strings or epoch seconds.
Nested:
subscriptions.<channel>— foremail,sms,whatsapp, etc. Values typicallysubscribed/unsubscribed.socialHandles.<network>— e.g.,socialHandles.telegram.attributes.<key>— whatever the merchant stuffed in.
Unknown fields don't error — they evaluate to "no match" for that contact, which under match: "all" excludes the contact entirely. Sanity-check filter fields before assuming an empty preview means "no VIPs".
List segments
GET /api/v1/audience-segments
Returns every segment in the workspace, most-recently-updated first.
{
"data": {
"segments": [
{
"id": "seg_01HX...",
"accountId": "acc_01HX...",
"name": "VIP subscribers",
"description": "Tagged vip or platinum with email subscribed",
"filter": { "match": "all", "rules": [ /* ... */ ] },
"cachedSize": 87,
"cachedSizeAt": "2026-05-13T10:30:00.000Z",
"createdAt": "2026-05-01T10:42:00.000Z",
"updatedAt": "2026-05-13T10:30:00.000Z"
}
]
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
cachedSize is the most recently computed member count. It's updated asynchronously after each filter change and refreshed on demand by /preview. Don't render it as authoritative — for "right now" counts, hit /preview.
Create a segment
POST /api/v1/audience-segments
Request body
| Field | Required | Notes |
|---|---|---|
name |
yes | 1–120 chars, unique per workspace. |
description |
no | ≤ 500 chars. |
filter |
yes | See the filter shape. |
| Status | error.code |
When |
|---|---|---|
400 |
VALIDATION |
Filter shape invalid, too many rules, unknown op. |
409 |
NAME_EXISTS |
Name collision. |
The cache size refresh kicks off asynchronously after a successful create — it's safe to immediately list, but cachedSize will lag for a few hundred milliseconds.
const seg = await ripllo.audienceSegments.create({
name: 'VIP subscribers',
filter: {
match: 'all',
rules: [
{ field: 'subscriptions.email', op: 'eq', value: 'subscribed' },
{ field: 'attributes.segment', op: 'in', value: ['vip', 'platinum'] },
],
},
});
Retrieve a segment
GET /api/v1/audience-segments/:id
Returns the full segment object.
Update a segment
PATCH /api/v1/audience-segments/:id
Partial. When filter changes, the cache refresh re-fires.
await ripllo.audienceSegments.update('seg_01HX...', {
filter: {
match: 'all',
rules: [{ field: 'createdAt', op: 'gte', value: '2026-01-01T00:00:00Z' }],
},
});
Delete a segment
DELETE /api/v1/audience-segments/:id
Hard delete. Any campaign that referenced this segment in its audience.segmentIds will resolve it to an empty contact set at send time — no error, just no recipients from that source.
Preview a saved segment
POST /api/v1/audience-segments/:id/preview
Resolves the segment against the current contact table and returns the count plus the first 20 contacts (minimal fields only: id, email, phone, firstName, lastName).
Body is empty — the segment's saved filter is used.
{
"data": {
"count": 87,
"sample": [
{
"id": "con_01HX...",
"email": "alice@example.com",
"phone": "+62811234567",
"firstName": "Alice",
"lastName": "Tan"
}
]
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
Preview an ad-hoc filter
POST /api/v1/audience-segments/preview
Same evaluation, but without persisting. Lets the audience UI show "this filter would match N contacts" while the merchant is still composing rules.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
filter |
object | yes | The filter shape, not yet saved. |
Response
{ "data": { "count": 412 }, "error": null, "meta": { ... } }
No sample contacts are returned from the ad-hoc preview — it's intentionally cheaper than the saved-segment preview. Save the segment first if you need a sample.
The audience segment object
| Field | Type | Nullable | Notes |
|---|---|---|---|
id |
string | no | seg_ + ULID. |
accountId |
string | no | Owning workspace. |
name |
string | no | Unique per workspace. |
description |
string | yes | |
filter |
object | no | The saved filter document. |
cachedSize |
integer | no | Most-recently computed match count. |
cachedSizeAt |
ISO 8601 | yes | When the cache was last refreshed. |
createdAt, updatedAt |
ISO 8601 | no |
Events
The audience segments resource doesn't emit outbox events. Segment lifecycle is internal to the merchant's workspace; downstream consumers (campaigns) re-resolve on demand at send time, so there's nothing to broadcast.
Pitfalls
- Nested-field typos.
attribute.segment(singular) silently matches nothing because the underlying field isattributes. The filter builder doesn't typo-check against your data. - Stale
cachedSize. OK for dashboard chrome; hit/previewfor send-time accuracy. gte/lteagainst ISO strings. Postgres compares lexicographically when both sides are strings; ISO 8601 happens to be lexicographically orderable for same-timezone values, but if your data mixes timezones, normalise to UTC before comparing.
Next
- Contacts — the rows segments filter over.
- Contact lists — the static counterpart.
- Marketing campaigns — where segments turn into delivered messages.