> ## 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.

# Add, List, and Remove Students in Your Academy API

> List, add, retrieve, and remove students in your academy. Supports bulk course enrollment on creation and detailed progress tracking per student.

The students endpoints let you manage the people who learn in your academy. You can list all students with pagination, add new students and optionally enroll them in courses at the same time, retrieve a single student's profile and detailed progress, and remove a student from your academy. All endpoints require your API key in the `Authorization` header. See [Authentication](/docs/api-reference/authentication).

***

## List students

Returns a paginated list of active students in your academy.

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

### Query parameters

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

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

### Response fields

<ResponseField name="data" type="object" required>
  <Expandable title="properties" defaultOpen>
    <ResponseField name="students" type="object[]" required>
      Array of student objects, ordered by `joined_at` descending.

      <Expandable title="student 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">
          Student's display name. May be `null` if not set.
        </ResponseField>

        <ResponseField name="avatar_url" type="string">
          URL of the student's avatar image. May be `null`.
        </ResponseField>

        <ResponseField name="joined_at" type="string" required>
          ISO 8601 timestamp of when the student joined the academy.
        </ResponseField>

        <ResponseField name="access" type="object" required>
          Summary of what the student holds through your academy's offers and purchases.

          <Expandable title="access properties">
            <ResponseField name="holds_pass" type="boolean" required>
              `true` when the student holds your Academy Pass (access to every course).
            </ResponseField>

            <ResponseField name="offer_count" type="number" required>
              Number of active offers the student currently holds, the Pass included.
            </ResponseField>

            <ResponseField name="has_purchase" type="boolean" required>
              `true` when the student has at least one paid, non-refunded payment in your academy.
            </ResponseField>
          </Expandable>
        </ResponseField>

        <ResponseField name="courses_enrolled" type="number" required>
          Number of active course enrollments.
        </ResponseField>

        <ResponseField name="enrollments" type="object[]" required>
          Array of active enrollment summaries. An enrollment means the student started the course (it appears in their My Learning); it is not the full picture of what they can open. A student can have access to courses they have not started yet, through free courses, offers they hold, or purchases.

          <Expandable title="enrollment properties">
            <ResponseField name="id" type="string" required>
              UUID of the enrollment record.
            </ResponseField>

            <ResponseField name="course_id" type="string" required>
              UUID of the enrolled course.
            </ResponseField>

            <ResponseField name="enrolled_at" type="string" required>
              ISO 8601 timestamp of enrollment.
            </ResponseField>

            <ResponseField name="completed_at" type="string">
              ISO 8601 timestamp of completion. `null` if not yet completed.
            </ResponseField>
          </Expandable>
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="pagination" type="object" required>
      <Expandable title="properties" defaultOpen>
        <ResponseField name="total" type="number" required>
          Total number of students in the academy.
        </ResponseField>

        <ResponseField name="limit" type="number" required>
          The `limit` value used for this request.
        </ResponseField>

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

### Example request

```bash theme={null}
curl "https://yourname.fayneos.com/api/v1/students?limit=10&offset=0" \
  -H "Authorization: Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

### Example response

```json theme={null}
{
  "data": {
    "students": [
      {
        "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
        "email": "alex@example.com",
        "name": "Alex Rivera",
        "avatar_url": "https://cdn.example.com/avatars/alex.jpg",
        "joined_at": "2025-03-15T09:22:00Z",
        "access": {
          "holds_pass": false,
          "offer_count": 1,
          "has_purchase": true
        },
        "courses_enrolled": 2,
        "enrollments": [
          {
            "id": "e1f2a3b4-c5d6-7890-efab-123456789abc",
            "course_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            "enrolled_at": "2025-03-15T09:22:00Z",
            "completed_at": null
          }

        ]
      }
    ],
    "pagination": {
      "total": 42,
      "limit": 10,
      "offset": 0
    }
  }
}
```

***

## Add a student

Adds a new student to your academy and optionally enrolls them in one or more courses in a single request.

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

### Body parameters

<ParamField body="email" type="string" required>
  Email address of the student. Must be a valid email format.
</ParamField>

<ParamField body="name" type="string">
  Display name for the student. Maximum 200 characters. Optional.
</ParamField>

<ParamField body="course_ids" type="string[]">
  Array of course UUIDs to enroll the student in. Maximum 50 course IDs per request. Each course must be published. Optional. Creates a permanent enrollment per course.
</ParamField>

<ParamField body="list_ids" type="string[]">
  Array of [list](/docs/api-reference/lists) UUIDs to add the student to. Maximum 50 per request. Each list must exist in this academy, or the request returns `400 invalid_lists`. Offer-managed lists return `400 list_managed_by_offer`; grant the offer instead. All ids are validated before the student account is created, so a rejected request creates nothing. Use `list_ids` for cohort or tier access you may want to revoke later, and `course_ids` for an individual course assignment. Optional.
</ParamField>

<ParamField body="send_welcome_email" default="true" type="boolean">
  Whether to send the magic-link welcome email to the new student. Set to `false` for silent imports. Optional. If your app signs students in with [sign-in links](/docs/api-reference/partner-sso), set it to `false`: minting a sign-in link invalidates the link in the welcome email, so the email would arrive with a dead button.
</ParamField>

### Response fields

Returns the newly created student with a `201` status code. The response contains `id`, `email`, `name`, a `membershipStatus` field (`created` or `reactivated`), an `accountCreated` boolean (`true` when this request created a brand-new Fayne account, `false` when the email already had one, for example from another academy), and an `enrollments` array whose items have `id`, `course_id`, and `course_title`. This is a smaller object than the list endpoint's student: it does not include `avatar_url`, `joined_at`, or `courses_enrolled`. Adding a student who is already an active member of the academy returns `409 already_exists`. The error body does not include the existing student's `id`, so to backfill ids for people who are already members, page through [`GET /students`](#list-students) and match on email.

### Example request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://yourname.fayneos.com/api/v1/students \
    -H "Authorization: Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "email": "jamie@example.com",
      "name": "Jamie Chen",
      "course_ids": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://yourname.fayneos.com/api/v1/students', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email: 'jamie@example.com',
      name: 'Jamie Chen',
      course_ids: ['a1b2c3d4-e5f6-7890-abcd-ef1234567890']
    })
  });
  const { data } = await response.json();
  ```
</CodeGroup>

***

## Get a student

Returns the full profile and enrollment detail for a single student, including per-course progress.

```text theme={null}
GET /api/v1/students/:studentId
```

### Path parameters

<ParamField path="studentId" type="string" required>
  UUID of the student. Must be a valid UUID or the request returns a `400`.
</ParamField>

### Response fields

<ResponseField name="data" type="object" required>
  <Expandable title="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">
      Student's display name. May be `null`.
    </ResponseField>

    <ResponseField name="avatar_url" type="string">
      URL of the student's avatar image. May be `null`.
    </ResponseField>

    <ResponseField name="joined_at" type="string" required>
      ISO 8601 timestamp of when the student joined the academy.
    </ResponseField>

    <ResponseField name="access" type="object" required>
      What the student holds through offers, purchases, and course assignments.

      <Expandable title="access properties" defaultOpen>
        <ResponseField name="holds_pass" type="boolean" required>
          `true` when the student holds your Academy Pass (access to every course).
        </ResponseField>

        <ResponseField name="offer_count" type="number" required>
          Number of active offers the student currently holds, the Pass included.
        </ResponseField>

        <ResponseField name="has_purchase" type="boolean" required>
          `true` when the student has at least one paid, non-refunded payment in your academy.
        </ResponseField>

        <ResponseField name="grants" type="object[]" required>
          The student's live course assignments (given through this API or the dashboard). A grant can exist before the student has opened the course, so it may not have a matching enrollment yet.

          <Expandable title="grant properties">
            <ResponseField name="course_id" type="string" required>
              UUID of the assigned course.
            </ResponseField>

            <ResponseField name="granted_at" type="string" required>
              ISO 8601 timestamp of when the course was assigned.
            </ResponseField>
          </Expandable>
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="enrollments" type="object[]" required>
      Array of active enrollments with full progress detail, ordered by `enrolled_at` descending. An enrollment means the student started the course; access itself can also come from free courses, offers, or purchases that have no enrollment yet.

      <Expandable title="enrollment properties" defaultOpen>
        <ResponseField name="id" type="string" required>
          UUID of the enrollment record.
        </ResponseField>

        <ResponseField name="course_id" type="string" required>
          UUID of the enrolled course.
        </ResponseField>

        <ResponseField name="course_title" type="string" required>
          Display title of the course.
        </ResponseField>

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

        <ResponseField name="enrolled_at" type="string" required>
          ISO 8601 timestamp of enrollment.
        </ResponseField>

        <ResponseField name="completed_at" type="string">
          ISO 8601 timestamp of course completion. `null` if not yet completed.
        </ResponseField>

        <ResponseField name="total_lessons" type="number" required>
          Total number of published lessons in the course.
        </ResponseField>

        <ResponseField name="completed_lessons" type="number" required>
          Number of lessons the student has completed.
        </ResponseField>

        <ResponseField name="progress" type="number" required>
          Completion percentage from `0` to `100`, rounded to the nearest integer.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

### Example request

```bash theme={null}
curl https://yourname.fayneos.com/api/v1/students/c3d4e5f6-a7b8-9012-cdef-123456789012 \
  -H "Authorization: Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

### Example response

```json theme={null}
{
  "data": {
    "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
    "email": "alex@example.com",
    "name": "Alex Rivera",
    "avatar_url": "https://cdn.example.com/avatars/alex.jpg",
    "joined_at": "2025-03-15T09:22:00Z",
    "access": {
      "holds_pass": false,
      "offer_count": 1,
      "has_purchase": true,
      "grants": [
        {
          "course_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "granted_at": "2025-03-15T09:22:00Z"
        }
      ]
    },
    "enrollments": [
      {
        "id": "e1f2a3b4-c5d6-7890-efab-123456789abc",
        "course_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "course_title": "Cold Outreach Mastery",
        "course_slug": "cold-outreach-mastery",
        "enrolled_at": "2025-03-15T09:22:00Z",
        "completed_at": null,
        "total_lessons": 12,
        "completed_lessons": 5,
        "progress": 42
      }
    ]
  }
}
```

***

## Mint a sign-in link

Mints a one-time authenticated sign-in URL for a student, so your own app can hand a user straight into the academy without a password or email code. Because your API key created the student, it already vouches for who they are, so redeeming the link signs the student in directly. See the [Partner sign-in guide](/docs/api-reference/partner-sso) for the full integration recipe.

```text theme={null}
POST /api/v1/students/:studentId/sign-in-link
```

### Path parameters

<ParamField path="studentId" type="string" required>
  UUID of the student to sign in. Must be an active member of this academy.
</ParamField>

### Body parameters

<ParamField body="next" default="/courses" type="string">
  Relative in-academy path to land on after sign-in, for example `/courses/a1b2c3d4-e5f6-7890-abcd-ef1234567890`. Absolute URLs are rejected with `400 invalid_next`. Optional.
</ParamField>

### Response fields

<ResponseField name="data" type="object" required>
  <Expandable title="properties" defaultOpen>
    <ResponseField name="url" type="string" required>
      The one-time sign-in URL on the academy's live host (your custom domain when one is verified). Redirect the user's browser here immediately.
    </ResponseField>

    <ResponseField name="single_use" type="boolean" required>
      Always `true`. The link is consumed on first use.
    </ResponseField>

    <ResponseField name="expires_in_seconds" type="number" required>
      Lifetime of the link, currently `3600` (one hour).
    </ResponseField>
  </Expandable>
</ResponseField>

<Warning>
  Mint on click, server-side only, and redirect immediately. Never render the URL into a page, cache it, log it, or email it. Each student has at most **one** outstanding sign-in link. Minting a new one invalidates any previous one, including a login code the student may have just requested by email and the link in their welcome email.
</Warning>

### Example request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://yourname.fayneos.com/api/v1/students/c3d4e5f6-a7b8-9012-cdef-123456789012/sign-in-link \
    -H "Authorization: Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{ "next": "/courses" }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    'https://yourname.fayneos.com/api/v1/students/c3d4e5f6-a7b8-9012-cdef-123456789012/sign-in-link',
    {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ next: '/courses' })
    }
  );
  const { data } = await response.json();
  // 302-redirect the user's browser to data.url
  ```
</CodeGroup>

### Example response

```json theme={null}
{
  "data": {
    "url": "https://yourname.fayneos.com/api/auth/confirm?next=%2Fcourses&token_hash=...&type=magiclink",
    "single_use": true,
    "expires_in_seconds": 3600
  }
}
```

Returns `201` on success. A consumed or expired link sends the student to the academy's sign-in page, where an email code gets them in and they then land on `next`. If their browser is already signed in to the academy, they land on `/courses` instead of `next`.

| Status | Code | When |
| - | - | - |
| `400` | `validation_error` | `studentId` is not a UUID, `next` is not a string or is longer than 500 characters, or the body is JSON but not an object (for example `null`) |
| `400` | `invalid_next` | `next` is not a relative in-academy path |
| `404` | `not_found` | The student is not an active member of this academy |
| `429` | `rate_limit_exceeded` | The key's rate limit is used up (see [Rate limiting](/docs/api-reference/authentication#rate-limiting)) |
| `502` | `link_mint_failed` | A transient auth-service failure. Retry once, then fall back to linking `/login` |

The shared `401`, `402`, and `500` errors from [Authentication](/docs/api-reference/authentication#error-codes) also apply.

***

## Remove a student

Removes a student from your academy. This revokes all of their course enrollments and the courses you assigned to them, removes them from every manual [list](/docs/api-reference/lists) (so they also lose any access that came through a list), and deletes all of their lesson progress. It cannot be undone. Purchase records are kept, and nothing is refunded. The student's Fayne account itself is not deleted; they are only removed from this academy.

```text theme={null}
DELETE /api/v1/students/:studentId
```

### Path parameters

<ParamField path="studentId" type="string" required>
  UUID of the student to remove.
</ParamField>

<Warning>
  Removing a student deletes all of their progress data in this academy, revokes their enrollments and course assignments, and takes them off your manual lists. This cannot be reversed. Their Fayne account is not deleted.
</Warning>

### Response fields

<ResponseField name="data" type="object" required>
  <Expandable title="properties" defaultOpen>
    <ResponseField name="removed" type="boolean" required>
      Always `true` when the operation succeeds.
    </ResponseField>
  </Expandable>
</ResponseField>

### Example request

```bash theme={null}
curl -X DELETE \
  https://yourname.fayneos.com/api/v1/students/c3d4e5f6-a7b8-9012-cdef-123456789012 \
  -H "Authorization: Bearer fa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

### Example response

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.