Currency

Audience segments

An audience segment is a stored filter over the merchant's contact book — "everyone on the newsletter list who came from the storefront import", "everyone tagged vip who hasn't unsubscribed from email". Where a contact list is static membership you mutate, a segment re-evaluates on every preview and every broadcast send. This page covers ripllo.audienceSegments on the Node SDK. For the underlying HTTP surface and the rule grammar, see API → Audience segments.

Namespace

ripllo.audienceSegments.list()
ripllo.audienceSegments.get(id)
ripllo.audienceSegments.create(input)
ripllo.audienceSegments.update(id, patch)
ripllo.audienceSegments.delete(id)
ripllo.audienceSegments.preview(id, input?)
ripllo.audienceSegments.previewAdhoc(input)

Standard CRUD plus two preview helpers. preview runs the segment by ID; previewAdhoc runs a filter that hasn't been saved yet (used by the dashboard's segment-builder UI to show live counts as you tweak the query).

The filter shape

Every write on this resource carries one filter object:

filter: {
  match: 'all' | 'any',        // required — 'all' = AND, 'any' = OR
  rules: [                     // up to 20 rules; a flat array, not a tree
    { field: '…', op: '…', value: … },
  ],
}

There is no top-level rules: { all: [...] } wrapper and no nesting — match and rules are siblings inside filter, and a body shaped the other way is rejected with 400 VALIDATION.

Fields and the operators each one accepts:

field Operators value
list in, not_in Array of contact-list IDs
tag in, not_in Array of tag names
subscription.<channel> eq, neq The state for that channel — subscribed, unsubscribed, pending or bounced. Channels are email, sms, whatsapp, …
createdAt gte, lte, gt, lt ISO 8601 date string (compared against Contact.createdAt)
source eq, neq, in, not_in Source string such as storlaunch_customer, manual_import, form_signup — an array for the *_in forms

The full operator enum is in, not_in, eq, neq, gte, lte, gt, lt. equals, not_equals, contains, not_contains, exists, between and within_days do not exist — a rule using one is rejected at validation.

A rule the resolver doesn't understand is dropped, not rejected. Validation only checks the op enum and the shape; the field/operator pairing above is applied later, and any combination outside the table (say field: 'tags' instead of 'tag', or createdAt with eq) resolves to nothing and is silently skipped. If every rule drops out, the segment matches every contact in the workspace. Always preview a new segment and sanity-check the count before you point a send at it.

Methods

audienceSegments.create

Signature. ripllo.audienceSegments.create(input): Promise<Segment>

const seg = await ripllo.audienceSegments.create({
  name: 'VIP — newsletter',
  description: 'High-value buyers who are still subscribed to email.',
  filter: {
    match: 'all',
    rules: [
      { field: 'tag', op: 'in', value: ['vip'] },
      { field: 'list', op: 'in', value: ['<newsletter-list-id>'] },
      { field: 'subscription.email', op: 'eq', value: 'subscribed' },
    ],
  },
});

name is unique per workspace — a duplicate returns 409 NAME_EXISTS. The cached size is refreshed in the background right after create.

audienceSegments.list

Signature. ripllo.audienceSegments.list(): Promise<{ segments: Segment[] }>

Returns every segment in the workspace, most-recently-updated first. No pagination — segment counts are typically small. This router does wrap its payload in { segments } (contact lists and channels do not — check per resource).

audienceSegments.get

Signature. ripllo.audienceSegments.get(id): Promise<Segment>

Fetches one segment by ID (an opaque cuid), including cachedSize and cachedAt from the last resolve.

audienceSegments.update

Signature. ripllo.audienceSegments.update(id, patch): Promise<Segment>

PATCH semantics over name, description and filter. Pass filter to replace the whole thing — filters aren't deep-merged, and a partial filter (e.g. rules without match) fails validation. Changing filter kicks off a cachedSize refresh.

await ripllo.audienceSegments.update('<segment-id>', {
  filter: {
    match: 'all',
    rules: [
      { field: 'tag', op: 'in', value: ['vip'] },
      { field: 'createdAt', op: 'lt', value: '2026-01-01T00:00:00Z' },
    ],
  },
});

audienceSegments.delete

Signature. ripllo.audienceSegments.delete(id): Promise<{ id: string; deleted: boolean }>

Hard delete. Nothing rewrites broadcasts that referenced the segment — a broadcast whose audience.segmentIds names a deleted segment simply resolves fewer contacts, and fails with EMPTY_AUDIENCE at send time if that leaves it with none. Clean up the references yourself.

audienceSegments.preview

Signature. ripllo.audienceSegments.preview(id, input?): Promise<{ count: number; sample: ContactSample[] }>

Runs the saved segment and returns a total count plus the first 20 matching contacts.

const preview = await ripllo.audienceSegments.preview('<segment-id>');
console.log(preview.count);
for (const c of preview.sample) console.log(c.email);

The sample is fixed at 20 — the input argument is accepted by the SDK but ignored by the server, so there is no way to widen it. Each sample entry is { id, email, phone, firstName, lastName }.

Preview does not refresh the segment's cachedSize; only create and a filter update do that.

audienceSegments.previewAdhoc

Signature. ripllo.audienceSegments.previewAdhoc(input): Promise<{ count: number }>

Runs an unsaved filter — the dashboard's segment-builder calls this while you edit to show "this matches 512 contacts". The body must wrap the filter under a filter key, and the response carries a count only, no sample.

const { count } = await ripllo.audienceSegments.previewAdhoc({
  filter: {
    match: 'any',
    rules: [
      { field: 'tag', op: 'in', value: ['vip'] },
      { field: 'source', op: 'in', value: ['storlaunch_customer', 'form_signup'] },
    ],
  },
});

Debounce on the client side — each call is a full COUNT over the contact table.

Types

interface Segment {
  id: string;                  // opaque cuid — no 'as_' prefix
  accountId: string;
  name: string;
  description: string | null;
  filter: SegmentFilter;
  cachedSize: number;          // last resolved size; 0 until first refresh
  cachedAt: string | null;
  createdAt: string;
  updatedAt: string;
}

interface SegmentFilter {
  match: 'all' | 'any';
  rules: SegmentRule[];        // max 20
}

interface SegmentRule {
  field: 'list' | 'tag' | `subscription.${string}` | 'createdAt' | 'source';
  op: 'in' | 'not_in' | 'eq' | 'neq' | 'gte' | 'lte' | 'gt' | 'lt';
  value?: unknown;
}

Common patterns

Build a "signed up before 2026" segment

await ripllo.audienceSegments.create({
  name: 'Pre-2026 subscribers',
  filter: {
    match: 'all',
    rules: [
      { field: 'createdAt', op: 'lt', value: '2026-01-01T00:00:00Z' },
      { field: 'subscription.email', op: 'eq', value: 'subscribed' },
    ],
  },
});

Date rules compare Contact.createdAt — there is no purchase-recency field in the v1 grammar, so "lapsed buyer" style targeting has to be expressed through tags or lists you maintain yourself.

Use a segment as a broadcast audience

const seg = await ripllo.audienceSegments.create({ name: 'May target', filter });
await ripllo.broadcasts.create({
  name: 'May promo',
  providers: ['email_resend'],
  content: { email: { subject: 'May promo', html: '…', text: '…' } },
  audience: { segmentIds: [seg.id] },
});

The broadcast re-runs the segment at send time; if you want a frozen audience, copy the previewed contacts into a static list first. Resolution is capped at 50,000 contacts per segment.

Live builder UI

let debounce: NodeJS.Timeout;
function onFilterChange(filter: SegmentFilter) {
  clearTimeout(debounce);
  debounce = setTimeout(async () => {
    const { count } = await ripllo.audienceSegments.previewAdhoc({ filter });
    setMatchCount(count);
  }, 200);
}

Errors

Code Status Cause
VALIDATION 400 Missing filter/match, more than 20 rules, or an op outside the eight-value enum.
NAME_EXISTS 409 A segment with that name already exists in the workspace.
NOT_FOUND 404 Segment doesn't exist in this workspace.
NO_ACCOUNT 403 Credential carries no accountId.

See Errors for handling.

Next

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