Currency

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 opin/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 singular subscription. prefix — so that rule is dropped. Drop every rule in a filter and the segment matches EVERYONE in the workspace, which a /send will then blast. Typos fail open, not closed.
  • Stale cachedSize. OK for dashboard chrome; hit /preview for send-time accuracy.
  • createdAt values must be ISO 8601 strings. They're parsed with new Date(...) and compared as real timestamps, so offsets like +07:00 are honoured correctly. But a number (epoch seconds) or an unparseable string makes the rule vanish instead of erroring — the segment silently gets bigger.

Next

CurrencyRupiah is paid by transfer or QRIS; US dollars settle through PayPal.