Currency

Contact lists

A contact list is a manually-curated collection of contacts. Unlike audience segments, list membership is static — a contact is in the list because someone (the merchant or a bulk-import job) explicitly added them, and stays in until explicitly removed.

Lists are how the merchant says "I want to email these 412 specific people the launch announcement". Segments are how the merchant says "I want to email everyone who matches this rule, whoever that turns out to be at send time". Both feed into marketing campaigns as audience sources.

Endpoints

Method Path Purpose
GET /api/v1/contact-lists List lists
POST /api/v1/contact-lists Create a list
GET /api/v1/contact-lists/:id Retrieve a list with members
DELETE /api/v1/contact-lists/:id Delete a list
POST /api/v1/contact-lists/:id/members Add members
DELETE /api/v1/contact-lists/:id/members/:contactId Remove a member

All endpoints require the merchant role.

List lists

GET /api/v1/contact-lists

Returns every list in the workspace, newest first. There's no pagination — the assumption is that merchants have tens of lists at most, not thousands.

{
  "data": [
    {
      "id": "clw3k9x2t0000v8f4qz7hm1ab",
      "accountId": "acc_01HX...",
      "name": "Newsletter subscribers",
      "description": "Opt-ins from the storefront footer form",
      "memberCount": 412,
      "createdAt": "2026-05-01T10:42:00.000Z",
      "updatedAt": "2026-05-13T08:11:00.000Z"
    }
  ],
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

memberCount is a denormalised counter updated transactionally on every add/remove. Trust it; don't count(*) over ContactListMember yourself.

Create a list

POST /api/v1/contact-lists

Request body

Field Type Required Notes
name string (1–120) yes Unique per workspace.
description string (≤500) | null no
Status error.code When
400 VALIDATION Shape wrong.
409 NAME_TAKEN A list with that name already exists in this workspace.
await ripllo.contactLists.create({
  name: 'Newsletter subscribers',
  description: 'Opt-ins from the storefront footer form',
});

Retrieve a list with members

GET /api/v1/contact-lists/:id

Returns the list metadata plus the most recently-added 100 members (full contact objects inlined). For deeper member listing, iterate via GET /contacts with a list filter (on the roadmap).

{
  "data": {
    "id": "clw3k9x2t0000v8f4qz7hm1ab",
    "accountId": "acc_01HX...",
    "name": "Newsletter subscribers",
    "description": "Opt-ins from the storefront footer form",
    "memberCount": 412,
    "members": [
      {
        "addedAt": "2026-05-12T15:33:00.000Z",
        "contact": { /* full Contact object */ }
      }
    ],
    "createdAt": "2026-05-01T10:42:00.000Z",
    "updatedAt": "2026-05-13T08:11:00.000Z"
  },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

Delete a list

DELETE /api/v1/contact-lists/:id

Hard delete. Members aren't deleted — only their membership rows in this list. Existing campaigns that targeted this list still record what they sent; they just can't be re-fanned to this audience anymore.

Add members

POST /api/v1/contact-lists/:id/members

Bulk-add up to 500 contacts to a list. Cross-workspace contact IDs are dropped — only contacts that belong to the same merchant as the list are added. The drop is silent only when at least one ID resolves; if none of them do, the call fails outright (see the errors table below).

Request body

Field Type Required Notes
contactIds string[] (1–500) yes con_… IDs.

Errors

Status error.code When
400 VALIDATION contactIds missing, empty, or longer than 500.
400 NO_VALID_CONTACTS None of the submitted IDs resolve to a contact in this workspace — all unknown, all cross-workspace, or a mix of the two. You get an error rather than { "added": 0 }, so don't treat "nothing matched" as a success case.
404 NOT_FOUND No such list in this workspace.

Response

{
  "data": { "added": 487 },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

added is the number of contacts that resolved inside this workspace and were upserted into the list (already-present contacts count too, since the upsert is idempotent). Use the difference between contactIds.length and added to detect partial cross-workspace drops — a total mismatch never reaches this response, it returns 400 NO_VALID_CONTACTS.

Adding a contact to a list also fires a list_added funnel trigger for that contact, which can kick off funnels wired to that event. This happens after the transaction commits, so trigger firing failures don't roll back the membership write.

await ripllo.contactLists.addMembers('clw3k9x2t0000v8f4qz7hm1ab', {
  contactIds: ['clw3k9y8p0001v8f4d2rj5nq0', 'clw3k9zb40002v8f4s7lt9wc3'],
});

Remove a member

DELETE /api/v1/contact-lists/:id/members/:contactId

Removes one contact from one list. Idempotent — removing a contact that's not in the list returns 200 OK with no change. The contact itself is not affected.

await ripllo.contactLists.removeMember('clw3k9x2t0000v8f4qz7hm1ab', 'clw3k9y8p0001v8f4d2rj5nq0');

The contact list object

Field Type Nullable Notes
id string no Opaque cuid — no prefix. Treat as an opaque string; do not parse or validate its shape.
accountId string no Owning workspace.
name string no Unique per workspace.
description string yes
memberCount integer no Denormalised. Trust it.
createdAt, updatedAt ISO 8601 no

Events

Event type Fires on Status
ripllo.contact_list.created.v1 New list created. Reserved — not currently emitted.
ripllo.contact_list.member_added.v1 Contact added to list (fires once per contact in bulk add). Reserved. The list_added funnel trigger uses an internal path, not the outbox.
ripllo.contact_list.member_removed.v1 Contact removed. Reserved.
ripllo.contact_list.deleted.v1 List deleted. Reserved.

Next

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