Currency

Contact lists

A contact list is a static grouping of contacts — a manually curated audience like "newsletter subscribers", "VIPs", or "Q1 2026 import". Lists are the simple cousin to audience segments: segments are dynamic queries that re-evaluate; lists are explicit membership rows you add and remove. This page covers ripllo.contactLists on the Node SDK. For the HTTP surface, see API → Contact lists.

Namespace

ripllo.contactLists.list()
ripllo.contactLists.get(id)
ripllo.contactLists.create(input)
ripllo.contactLists.addMember(id, input)
ripllo.contactLists.removeMember(id, contactId)
ripllo.contactLists.delete(id)

Six methods. There's no update, and no PATCH /api/v1/contact-lists/:id behind it either — a list's name and description are fixed once created, so renaming means creating the new list and re-adding members. Membership is mutated through addMember / removeMember, not by reassigning a list of IDs.

Methods

contactLists.create

Signature. ripllo.contactLists.create(input): Promise<ContactList>

Creates an empty list. Membership is added afterwards.

const list = await ripllo.contactLists.create({
  name: 'Newsletter — Indonesia',
  description: 'Buyers who opted in via the homepage modal.',
});

contactLists.list

Signature. ripllo.contactLists.list(): Promise<ContactList[]>

Returns every list in the workspace, newest first. No pagination — most workspaces have well under 100 lists. Each entry includes a memberCount integer so you don't have to count membership yourself.

const lists = await ripllo.contactLists.list();
for (const l of lists) console.log(l.name, l.memberCount);

The payload is a bare array, not { lists }. The SDK's return type says { lists }, but this router responds with the array itself, so const { lists } = await … yields undefined and the loop after it throws. Assign the result directly. (Wrapping is per-router here: audienceSegments.list() really does return { segments }.)

contactLists.get

Signature. ripllo.contactLists.get(id): Promise<ContactList>

Fetches by ID (an opaque cuid). Returns the list shape plus memberCount and a members array — the 100 most recently added membership rows, each with its contact inlined. There is no member-pagination endpoint; for a roster larger than 100 you'll need to hold your own copy as you add.

contactLists.addMember

Signature. ripllo.contactLists.addMember(id, input): Promise<{ added: number }>

Adds contacts to a list. Takes an array, up to 500 per call, and returns how many were upserted — not a boolean. Re-adding an existing member is a no-op that still counts toward added.

await ripllo.contactLists.addMember('<list-id>', { contactIds: ['<contact-id>', '<contact-id-2>'] });

The field is contactIds, plural and an array. The SDK types this parameter as { contactId: string }; that body fails zod validation with 400 VALIDATION, so the method is unusable as typed. Pass { contactIds: [...] }. If none of the IDs belong to your workspace you get 400 NO_VALID_CONTACTS.

contactLists.removeMember

Signature. ripllo.contactLists.removeMember(id, contactId): Promise<{ id: string; contactId: string }>

Removes a contact from a list and resolves to the pair it acted on. Removing a contact that wasn't a member is silently fine — there's no removed flag to check, so compare memberCount before and after if you need to know.

await ripllo.contactLists.removeMember('<list-id>', '<contact-id>');

contactLists.delete

Signature. ripllo.contactLists.delete(id): Promise<{ id: string }>

Hard-deletes the list and resolves to { id } (not { deleted: true }, despite the SDK's type). Membership rows go with it; the underlying contacts are not touched.

await ripllo.contactLists.delete('<list-id>');

Types

interface ContactList {
  id: string;                  // opaque cuid — no 'cl_' prefix
  accountId: string;
  name: string;
  description: string | null;
  memberCount: number;
  createdAt: string;
  updatedAt: string;
}

get also returns members — up to 100 rows of { listId, contactId, addedAt, contact }, newest first. list does not.

Common patterns

Bulk-add from a contact import

const list = await ripllo.contactLists.create({ name: 'CSV import 2026-05' });
const { contacts } = await ripllo.contacts.list({ limit: 100 });
await ripllo.contactLists.addMember(list.id, { contactIds: contacts.map((c) => c.id) });

One call takes up to 500 IDs, so batch rather than looping. If your audience is rule-shaped rather than a fixed roster, prefer an audience segment — it re-evaluates at send time instead of going stale.

Sync a list against an external CRM

async function syncList(listId: string, desiredContactIds: Set<string>) {
  // The 100 most recent members come back on `get`; there is no
  // member-pagination endpoint, so keep your own roster for larger lists.
  const { members } = await ripllo.contactLists.get(listId);
  const current = new Set(members.map((m) => m.contactId));

  const toAdd = [...desiredContactIds].filter((id) => !current.has(id));
  if (toAdd.length) await ripllo.contactLists.addMember(listId, { contactIds: toAdd });

  for (const id of current) {
    if (!desiredContactIds.has(id)) await ripllo.contactLists.removeMember(listId, id);
  }
}

Wire a list to a broadcast

const list = await ripllo.contactLists.create({ name: 'May newsletter audience' });
// ...add members...
await ripllo.broadcasts.create({
  name: 'May newsletter',
  providers: ['email_resend'],
  content: { email: { subject: 'Your May update', html: '…', text: '…' } },
  audience: { listIds: [list.id] },
});

audience.listIds is the seam between lists and sends — and it's on broadcasts, not marketingCampaigns. See broadcasts for the send-time mechanics.

Errors

Code Status Cause
VALIDATION 400 Bad name (empty / over 120 chars), or a members body that isn't { contactIds: [...] } with 1–500 entries.
NO_VALID_CONTACTS 400 None of the supplied contact IDs belong to this workspace.
NAME_TAKEN 409 A list with that name already exists in the workspace.
NOT_FOUND 404 List 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.