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": "cl_01HX...",
      "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": "cl_01HX...",
    "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 silently dropped — only contacts that belong to the same merchant as the list are added.

Request body

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

Response

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

added is the number of contacts that were actually inserted (already-present contacts count too, since the upsert is idempotent). Use the difference between contactIds.length and added to detect cross-workspace drops.

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('cl_01HX...', {
  contactIds: ['con_01HX...', 'con_02HX...'],
});

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('cl_01HX...', 'con_01HX...');

The contact list object

Field Type Nullable Notes
id string no cl_ + ULID.
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