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
- Contacts — the underlying records.
- Audience segments — the rules-based alternative to manual lists.
- Marketing campaigns — lists are one audience source.