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