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 opin/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/lte accept either ISO strings or epoch seconds.

Nested:

  • subscriptions.<channel> — for email, sms, whatsapp, etc. Values typically subscribed/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 is attributes. The filter builder doesn't typo-check against your data.
  • Stale cachedSize. OK for dashboard chrome; hit /preview for send-time accuracy.
  • gte/lte against 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