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 tag in ["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": "subscription.email", "op": "eq", "value": "subscribed" },
{ "field": "tag", "op": "in", "value": ["vip", "platinum"] },
{ "field": "source", "op": "neq", "value": "import" }
]
}
| 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) | One of the five recognised fields below. Any other string parses fine but is ignored by the resolver — see Supported fields. |
op |
enum | One of in, not_in, eq, neq, gte, lte, gt, lt. Which ops actually apply depends on the field. |
value |
any | Type depends on op — in/not_in take arrays; others take scalars. |
Supported fields
The resolver recognises exactly five fields. Each one only honours the operators listed here; any other field/operator pairing is dropped.
field |
Ops | value |
Matches |
|---|---|---|---|
list |
in, not_in |
array of contact-list ids | Contact is (not) a member of any of those contact lists. |
tag |
in, not_in |
array of tag names (not ids) | Contact carries (does not carry) any of those tags. |
subscription.<channel> |
eq, neq |
usually "subscribed" / "unsubscribed" |
JSON-path match on Contact.subscriptions[<channel>]. Channels: email, sms, whatsapp, etc. Note the singular subscription. prefix. |
createdAt |
gte, lte, gt, lt |
ISO 8601 string | Contact creation timestamp, compared as a real timestamp. Numbers (epoch seconds) are not accepted. |
source |
eq, neq, in, not_in |
string or array of strings | Contact.source. |
Nothing else is implemented today: email, phone, firstName, lastName, externalRef, updatedAt, subscriptions.* (plural), socialHandles.* and attributes.* are not supported rule fields. "In list X but not list Y" is expressed with two list rules under match: "all".
Unrecognised rules fail open, not closed. A rule the resolver doesn't recognise — unknown field, wrong operator for the field, an unparseable createdAt value — is silently dropped from the where clause rather than rejected. If every rule in a filter is dropped, the segment resolves to every contact in the workspace. A typo does not give you an empty preview; it gives you your whole contact base. Always /preview a filter before sending to it.
List segments
GET /api/v1/audience-segments
Returns every segment in the workspace, most-recently-updated first.
{
"data": {
"segments": [
{
"id": "clx3f8k2p0003qw3f6e5d4c3b",
"accountId": "acc_9f2c1b7a",
"name": "VIP subscribers",
"description": "Tagged vip or platinum with email subscribed",
"filter": { "match": "all", "rules": [ /* ... */ ] },
"cachedSize": 87,
"cachedAt": "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: 'subscription.email', op: 'eq', value: 'subscribed' },
{ field: 'tag', 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('clx3f8k2p0003qw3f6e5d4c3b', {
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": "clx3f8k2p0001qw3f2b9c4a5f",
"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 | Bare cuid (e.g. clx3f8k2p0003qw3f6e5d4c3b) — no prefix. |
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. |
cachedAt |
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
- Field typos widen the segment.
subscriptions.email(plural) isn't recognised — the resolver only matches the singularsubscription.prefix — so that rule is dropped. Drop every rule in a filter and the segment matches EVERYONE in the workspace, which a/sendwill then blast. Typos fail open, not closed. - Stale
cachedSize. OK for dashboard chrome; hit/previewfor send-time accuracy. createdAtvalues must be ISO 8601 strings. They're parsed withnew Date(...)and compared as real timestamps, so offsets like+07:00are honoured correctly. But a number (epoch seconds) or an unparseable string makes the rule vanish instead of erroring — the segment silently gets bigger.
Next
- Contacts — the rows segments filter over.
- Contact lists — the static counterpart.
- Marketing campaigns — where segments turn into delivered messages.