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

# Manage Student Lists and Cohort-Based Course Access

> Create lists of students, add and remove members by email, and use list membership to drive which courses students can access.

A **list** is a named group of students in your academy (a "cohort", "tier", or "segment"). Lists do two jobs:

* **Group students** so you can manage them in bulk from your own tools.
* **Drive course access.** When an offer in your academy grants its courses to a list, every member of that list can access those courses. Add someone to the list and they get access; remove them and the access is revoked. You configure offers in your dashboard; this API manages the list and its membership. For the start-to-finish recipe (lock courses, create a hidden free offer, grant it to a list, drive membership from here), see [Access Workflows](/docs/api-reference/guides).

<Note>
  List access is **dynamic** and follows membership. This is different from an [enrollment](/docs/api-reference/enrollments), which is a direct per-student course assignment. Use a list for cohort or tier access you may want to revoke later; use an enrollment for an individual assignment.
</Note>

<Warning>
  Some lists are **managed by an offer**: when a student buys or claims an offer, they join its member list automatically. Those lists are the offer's source of truth for who holds it, so this API refuses to rename or delete them, or to edit their membership, with a `400 list_managed_by_offer` error. Grant or revoke the offer from your dashboard instead. Reads work on every list.
</Warning>

All endpoints require your API key in the `Authorization` header. See [Authentication](/docs/api-reference/authentication).

***

## List all lists

Returns every list in your academy, newest first, each with its active member count.

```text theme={null}
GET /api/v1/lists
```

### Response fields

<ResponseField name="data" type="object" required>
  <Expandable title="properties" defaultOpen>
    <ResponseField name="lists" type="object[]" required>
      <Expandable title="list properties" defaultOpen>
        <ResponseField name="id" type="string" required>
          UUID of the list.
        </ResponseField>

        <ResponseField name="name" type="string" required>
          Display name of the list, unique within the academy.
        </ResponseField>

        <ResponseField name="description" type="string">
          Optional description. May be `null`.
        </ResponseField>

        <ResponseField name="member_count" type="number" required>
          Number of active members.
        </ResponseField>

        <ResponseField name="created_at" type="string" required>
          ISO 8601 creation timestamp.
        </ResponseField>

        <ResponseField name="updated_at" type="string" required>
          ISO 8601 last-updated timestamp.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

### Example request

```bash theme={null}
curl https://yourname.fayneos.com/api/v1/lists \
  -H "Authorization: Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

### Example response

```json theme={null}
{
  "data": {
    "lists": [
      {
        "id": "56b4c1c0-27e1-4bf5-ab8a-bca651aef88a",
        "name": "VIP Clients",
        "description": "For our VIPs",
        "member_count": 2,
        "created_at": "2026-05-28T21:19:08Z",
        "updated_at": "2026-05-28T21:19:08Z"
      }
    ]
  }
}
```

***

## Create a list

```text theme={null}
POST /api/v1/lists
```

### Body parameters

<ParamField body="name" type="string" required>
  Name of the list. 1 to 100 characters. Must be unique within the academy (case-insensitive); a duplicate returns `409 already_exists`.
</ParamField>

<ParamField body="description" type="string">
  Optional description. Maximum 500 characters.
</ParamField>

### Response

Returns the created list (`member_count` is `0`) with a `201` status code.

### Example request

```bash theme={null}
curl -X POST https://yourname.fayneos.com/api/v1/lists \
  -H "Authorization: Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "name": "VIP Cohort", "description": "Paying members" }'
```

### Example response

```json theme={null}
{
  "data": {
    "id": "9b34bee2-c132-4374-9c3f-d1e24817a301",
    "name": "VIP Cohort",
    "description": "Paying members",
    "member_count": 0,
    "created_at": "2026-06-19T11:12:52Z",
    "updated_at": "2026-06-19T11:12:52Z"
  }
}
```

***

## Get a list

```text theme={null}
GET /api/v1/lists/:listId
```

### Path parameters

<ParamField path="listId" type="string" required>
  UUID of the list. Returns `404 not_found` if it does not exist in this academy.
</ParamField>

Returns the same list object shape as the list endpoint.

***

## Update a list

Rename a list or change its description. Provide at least one of `name` or `description`.

```text theme={null}
PATCH /api/v1/lists/:listId
```

### Body parameters

<ParamField body="name" type="string">
  New name. 1 to 100 characters. A duplicate name returns `409 already_exists`.
</ParamField>

<ParamField body="description" type="string">
  New description, or `null` to clear it. Maximum 500 characters.
</ParamField>

Returns the updated list object.

### Example request

```bash theme={null}
curl -X PATCH https://yourname.fayneos.com/api/v1/lists/9b34bee2-c132-4374-9c3f-d1e24817a301 \
  -H "Authorization: Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "name": "VIP Cohort 2026" }'
```

***

## Delete a list

Deletes the list. Members are not removed from the academy, but they lose any course access that this list was granting. Offer-managed lists cannot be deleted through the API (`400 list_managed_by_offer`).

```text theme={null}
DELETE /api/v1/lists/:listId
```

### Example response

```json theme={null}
{ "data": { "deleted": true } }
```

***

## List members

Returns the active members of a list, newest first.

```text theme={null}
GET /api/v1/lists/:listId/members
```

### Query parameters

<ParamField query="limit" default="50" type="number">
  Number of members to return. Minimum `1`, maximum `100`.
</ParamField>

<ParamField query="offset" default="0" type="number">
  Number of members to skip. Use with `limit` for pagination.
</ParamField>

### Response fields

<ResponseField name="data" type="object" required>
  <Expandable title="properties" defaultOpen>
    <ResponseField name="members" type="object[]" required>
      <Expandable title="member properties" defaultOpen>
        <ResponseField name="id" type="string" required>
          UUID of the student's user account.
        </ResponseField>

        <ResponseField name="email" type="string" required>
          Student's email address.
        </ResponseField>

        <ResponseField name="name" type="string">
          Display name. May be `null`.
        </ResponseField>

        <ResponseField name="avatar_url" type="string">
          Avatar URL. May be `null`.
        </ResponseField>

        <ResponseField name="joined_at" type="string" required>
          ISO 8601 timestamp of when they joined the list.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="pagination" type="object" required>
      <Expandable title="properties">
        <ResponseField name="total" type="number" required>
          Total active members.
        </ResponseField>

        <ResponseField name="limit" type="number" required>
          The `limit` used.
        </ResponseField>

        <ResponseField name="offset" type="number" required>
          The `offset` used.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

***

## Add members

Adds students to a list **by email**. This endpoint is an upsert: if an email does not yet belong to a student in your academy, the student is created first, then added to the list. Calling it again for someone already on the list is a safe no-op. Offer-managed lists cannot be edited through the API (`400 list_managed_by_offer`); grant the offer instead.

```text theme={null}
POST /api/v1/lists/:listId/members
```

### Body parameters

<ParamField body="email" type="string">
  A single student email. Provide either `email` or `emails`.
</ParamField>

<ParamField body="emails" type="string[]">
  A batch of student emails. Maximum 100 per request. Provide either `email` or `emails`.
</ParamField>

<ParamField body="send_welcome_email" default="true" type="boolean">
  Whether to send the magic-link welcome email to students who are newly created by this call. Set to `false` for silent bulk syncs. Has no effect on people who are already members.
</ParamField>

### Response fields

Returns `200` with a per-email result array, so a single failure (such as a plan-limit hit) does not fail the whole batch.

<ResponseField name="data" type="object" required>
  <Expandable title="properties" defaultOpen>
    <ResponseField name="results" type="object[]" required>
      <Expandable title="result properties" defaultOpen>
        <ResponseField name="email" type="string" required>
          The email from the request.
        </ResponseField>

        <ResponseField name="status" type="string" required>
          One of: `created` (a new student was provisioned and added), `added` (an existing student was added to the list), `already_member` (already on the list, no change), or `error`.
        </ResponseField>

        <ResponseField name="student_id" type="string">
          UUID of the student. Present unless `status` is `error`.
        </ResponseField>

        <ResponseField name="code" type="string">
          Error code (for example `limit_exceeded`). Present only when `status` is `error`.
        </ResponseField>

        <ResponseField name="message" type="string">
          Human-readable error detail. Present only when `status` is `error`.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

### Example request

```bash theme={null}
curl -X POST https://yourname.fayneos.com/api/v1/lists/9b34bee2-c132-4374-9c3f-d1e24817a301/members \
  -H "Authorization: Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "emails": ["jamie@example.com", "alex@example.com"], "send_welcome_email": false }'
```

### Example response

```json theme={null}
{
  "data": {
    "results": [
      { "email": "jamie@example.com", "status": "created", "student_id": "6bf451b8-765c-412c-a707-e4d0c6b4de2f" },
      { "email": "alex@example.com", "status": "already_member", "student_id": "c0b9a270-b89a-425a-80e0-869dfdc3d6d5" }
    ]
  }
}
```

***

## Remove a member

Removes a student from a list. They remain in your academy, but they lose any course access this list was granting. The student's enrollments and progress are untouched. Offer-managed lists cannot be edited through the API (`400 list_managed_by_offer`); revoke the offer instead.

```text theme={null}
DELETE /api/v1/lists/:listId/members/:userId
```

### Path parameters

<ParamField path="userId" type="string" required>
  UUID of the student to remove (the `id` from the members list). Returns `404 not_found` if they are not an active member of the list.
</ParamField>

### Example response

```json theme={null}
{ "data": { "removed": true } }
```

***

## Courses a list grants

Returns the courses this list grants access to, derived from your academy's active offers. A list grants a course in one of two ways: an offer is free for the list's members (the offer grants to the list), or the list is the offer's member list (the people who bought or claimed it). This is read-only; create or edit offers in your dashboard.

```text theme={null}
GET /api/v1/lists/:listId/courses
```

### Response fields

<ResponseField name="data" type="object" required>
  <Expandable title="properties" defaultOpen>
    <ResponseField name="courses" type="object[]" required>
      <Expandable title="course properties" defaultOpen>
        <ResponseField name="course_id" type="string" required>
          UUID of the course.
        </ResponseField>

        <ResponseField name="title" type="string" required>
          Course title.
        </ResponseField>

        <ResponseField name="slug" type="string" required>
          URL-safe course slug.
        </ResponseField>

        <ResponseField name="granted_via" type="object[]" required>
          The offers through which this list grants the course.

          <Expandable title="granted_via properties">
            <ResponseField name="offer_id" type="string" required>
              UUID of the offer.
            </ResponseField>

            <ResponseField name="offer_name" type="string" required>
              Display name of the offer.
            </ResponseField>

            <ResponseField name="role" type="string" required>
              How the list is attached: `grants_to` (the offer is free for this list's members) or `member` (the list is the offer's member list).
            </ResponseField>
          </Expandable>
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

### Example response

```json theme={null}
{
  "data": {
    "courses": [
      {
        "course_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "title": "Cold Outreach Mastery",
        "slug": "cold-outreach-mastery",
        "granted_via": [
          {
            "offer_id": "d4e5f6a7-b8c9-0123-def0-456789abcdef",
            "offer_name": "VIP Bundle",
            "role": "grants_to"
          }
        ]
      }
    ]
  }
}
```
