> ## Documentation Index
> Fetch the complete documentation index at: https://help.retainful.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Lists API

> Manage list membership programmatically.

Lists (contact groups) organize your audience — see the [merchant-side guide](/audience/lists) for the concepts. The API lets your systems manage membership: add a customer to "Active subscribers" when they start a plan, remove them when they cancel.

All requests require [authentication](/developers/authentication).

## List your lists

`GET /api/v1/contact-groups`

Returns the lists and segments in your organization, with their IDs. Use this to find the list ID you need for the add-contact call.

| Parameter | Default | Notes                                               |
| --------- | ------- | --------------------------------------------------- |
| `page`    | `1`     | Page number.                                        |
| `limit`   | `10`    | Items per page. Values above 100 are capped at 100. |
| `type`    | `ALL`   | `LIST`, `SEGMENT`, or `ALL`.                        |

```bash theme={null}
curl "https://api.retainful.net/api/v1/contact-groups?type=LIST" \
  -H "X-API-Key: $RETAINFUL_API_KEY"
```

`GET /api/v1/contact-groups/{id}` returns a single list.

## Add a contact to a list

`POST /api/v1/contact-groups/{listId}/contacts`

Creates or updates the contact **and** adds them to the list, in one call. Returns `200 OK`.

The body is a **flat** contact object — the same field set as the [Contacts API](/developers/api/contacts). Do not wrap it in a `contact` object and do not send an array; both are rejected with `400`.

<ParamField body="email" type="string" required>
  The contact's email address. Required on this endpoint, even though the Contacts API accepts phone instead.
</ParamField>

<ParamField body="first_name" type="string">
  Max 100 characters.
</ParamField>

<ParamField body="last_name" type="string">
  Max 100 characters.
</ParamField>

<ParamField body="email_opt_in" type="string">
  `SUBSCRIBED`, `NON_SUBSCRIBED`, or `UNSUBSCRIBED`. See the warning below — always set this explicitly.
</ParamField>

<ParamField body="phone" type="string">
  7–20 characters.
</ParamField>

<ParamField body="source" type="string">
  Where the contact came from. Defaults to `api`.
</ParamField>

Every other [contact field](/developers/api/contacts#request) is accepted here too.

```bash Example request theme={null}
curl -X POST "https://api.retainful.net/api/v1/contact-groups/019e92d4-5856-7c11-9feb-a3200364d61e/contacts" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $RETAINFUL_API_KEY" \
  -d '{
    "email": "jane@example.com",
    "first_name": "Jane",
    "last_name": "Smith",
    "email_opt_in": "SUBSCRIBED",
    "source": "web-store"
  }'
```

### Response

```json 200 OK theme={null}
{
  "message": "Successfully created contact and added to the contact group",
  "contact": {
    "email": "jane@example.com",
    "contactUUID": "01K8J5M02QYPEJE5YN3V944ZY9",
    "action": "created"
  }
}
```

`action` is `created` or `updated`, telling you whether the contact already existed. Note this endpoint returns a plain object — unlike the Contacts API, which returns a JSON:API envelope.

A `404` means the list ID doesn't exist. The contact may still have been created; it just isn't in that list.

<Warning>
  **Always send `email_opt_in` explicitly on this endpoint.** If you omit it, the result depends on whether the contact already existed: a brand-new contact is created as `NON_SUBSCRIBED`, but an existing contact is set to `SUBSCRIBED`. The same request body produces different consent outcomes depending on state you can't see.
</Warning>

## List membership as a trigger

Joining a list can [trigger an automation](/automations/triggers) — which makes this API a simple, robust way to start flows from your backend:

1. Create a list like "Trial started" in the dashboard.
2. Build a welcome/onboarding automation triggered by **Subscribed to list → Trial started**.
3. From your backend, add users to the list when their trial begins.

The trigger fires once per contact per list. Adding someone who's already a member won't fire it again.

<Note>
  For richer use cases — where the triggering moment carries data you want in filters and emails — prefer [custom events](/developers/api/events). Lists-as-triggers shine when the only fact that matters is membership itself.
</Note>

## Membership vs. consent

Adding someone to a list does **not** make them marketable — their [subscription status](/audience/overview#subscription-status) still governs whether marketing email reaches them. Manage consent through the `email_opt_in` field.

## Contacts not showing up?

Contacts are written synchronously — if you got a success response with a `contactUUID`, the contact exists. If you can't see it:

1. **Check `action` in the response.** A response without a `contact` object means you're not hitting this endpoint — verify the URL.
2. **Verify the list ID.** A wrong ID returns `404`; look it up with `GET /api/v1/contact-groups`.
3. **Match the API key to the organization** you're viewing in the dashboard. A key belongs to exactly one organization.
4. **Check `email_opt_in`.** Contacts created as `NON_SUBSCRIBED` are hidden by the default subscription filters on the Members tab.
5. **Clear filters on the Members view**, and hard-refresh — the dashboard caches client-side.
