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

# Sync students from your LMS

> Bring your existing roster — once, daily, or in real time.

If you already have students in your LMS or your CRM, you don't need to
re-enroll them in Hawkings by hand. Pick the strategy that matches how
much sync you need.

## Option A: One-time bulk import

For a single migration:

<CodeGroup>
  ```ts TypeScript theme={null}
  import { parse } from "csv-parse/sync";
  import { readFileSync } from "node:fs";

  const rows = parse(readFileSync("students.csv"), { columns: true });

  await hk.cohorts.bulk({
    rows: rows.map(r => ({
      cohort_id: "coh_123",
      student_email: r.email,
      student_name: r.full_name,
      external_id: r.canvas_user_id,    // your LMS' id, stored as metadata
    })),
  });
  ```

  ```php PHP theme={null}
  $rows = array_map('str_getcsv', file('students.csv'));
  $headers = array_shift($rows);
  $students = array_map(fn ($r) => array_combine($headers, $r), $rows);

  $hk->cohorts->bulk([
      'rows' => array_map(fn ($r) => [
          'cohort_id' => 'coh_123',
          'student_email' => $r['email'],
          'student_name' => $r['full_name'],
          'external_id' => $r['canvas_user_id'],    // your LMS' id, stored as metadata
      ], $students),
  ]);
  ```
</CodeGroup>

`bulk` is idempotent on `(cohort_id, student_email)` — re-running won't
duplicate.

## Option B: Daily reconcile

Run a cron job that pulls your LMS' roster and applies a diff:

<CodeGroup>
  ```ts TypeScript theme={null}
  import { Hawkings } from "@hawkings/sdk";
  import { Canvas } from "@canvas/sdk";

  const hk = new Hawkings();
  const canvas = new Canvas(/* ... */);

  async function reconcile(cohort_id: string, canvas_course_id: string) {
    const [hkStudents, lmsStudents] = await Promise.all([
      hk.students.list({ cohort_id: cohortId }).then(p => p.data),
      canvas.users.list({ course_id: canvasCourseId, role: "student" }),
    ]);

    const lmsByEmail = new Map(lmsStudents.map(s => [s.email, s]));
    const hkByEmail  = new Map(hkStudents.map(s => [s.email, s]));

    const toAdd    = lmsStudents.filter(s => !hkByEmail.has(s.email));
    const toRemove = hkStudents.filter(s => !lmsByEmail.has(s.email));

    await hk.cohorts.bulk({
      rows: toAdd.map(s => ({ cohort_id: cohortId, student_email: s.email, student_name: s.name })),
    });

    for (const s of toRemove) {
      await hk.cohorts.unenroll(cohortId, s.id);
    }
  }
  ```

  ```php PHP theme={null}
  $hk = new \Hawkings\Client();
  $canvas = new \Canvas\Client(/* ... */);

  function reconcile(string $cohortId, string $canvasCourseId): void {
      global $hk, $canvas;

      $hkStudents = $hk->students->list(['cohort_id' => $cohortId])->data;
      $lmsStudents = $canvas->users->list([
          'course_id' => $canvasCourseId,
          'role' => 'student',
      ]);

      $lmsByEmail = [];
      foreach ($lmsStudents as $s) { $lmsByEmail[$s->email] = $s; }
      $hkByEmail = [];
      foreach ($hkStudents as $s) { $hkByEmail[$s->email] = $s; }

      $toAdd    = array_filter($lmsStudents, fn ($s) => !isset($hkByEmail[$s->email]));
      $toRemove = array_filter($hkStudents,  fn ($s) => !isset($lmsByEmail[$s->email]));

      $hk->cohorts->bulk([
          'rows' => array_map(fn ($s) => [
              'cohort_id' => $cohortId,
              'student_email' => $s->email,
              'student_name' => $s->name,
          ], array_values($toAdd)),
      ]);

      foreach ($toRemove as $s) {
          $hk->cohorts->unenroll($cohortId, $s->id);
      }
  }
  ```
</CodeGroup>

We recommend running this nightly. Reconciliation is cheap; the only
billable side effect is the AI generation kicked off when a brand-new
student joins.

## Option C: Real-time webhooks (LMS → Hawkings)

If your LMS supports outbound webhooks, point them at your backend and
forward to Hawkings:

<CodeGroup>
  ```ts TypeScript theme={null}
  // Express handler for Canvas' enrollment webhook
  app.post("/webhooks/canvas/enrollment", async (req, res) => {
    const { event, user, course } = req.body;

    const cohortId = await mapCanvasCourseToCohort(course.id);

    if (event === "enrollment.created") {
      await hk.cohorts.bulk({
        rows: [{
          cohort_id: cohortId,
          student_email: user.email,
          student_name: user.name,
          external_id: user.id,
        }],
      });
    }

    if (event === "enrollment.deleted") {
      await hk.cohorts.unenroll(cohortId, user.email);
    }

    res.sendStatus(200);
  });
  ```

  ```php PHP theme={null}
  // Laravel handler for Canvas' enrollment webhook
  Route::post('/webhooks/canvas/enrollment', function (Request $request) use ($hk) {
      $event  = $request->input('event');
      $user   = (object) $request->input('user');
      $course = (object) $request->input('course');

      $cohortId = mapCanvasCourseToCohort($course->id);

      if ($event === 'enrollment.created') {
          $hk->cohorts->bulk([
              'rows' => [[
                  'cohort_id' => $cohortId,
                  'student_email' => $user->email,
                  'student_name' => $user->name,
                  'external_id' => $user->id,
              ]],
          ]);
      }

      if ($event === 'enrollment.deleted') {
          $hk->cohorts->unenroll($cohortId, $user->email);
      }

      return response()->noContent();
  });
  ```
</CodeGroup>

## Option D: First-class Moodle bridge

If you're on Moodle, install the Hawkings plugin. It handles enrollment,
grade-passback, and SCORM in one shot. See [the Moodle integration guide](https://hawkings.education/integrations/moodle).

## Storing external IDs

Always pass `external_id` (or a `metadata` object) when creating users
or cohorts. It lets you:

<CodeGroup>
  ```ts TypeScript theme={null}
  const sub = await hk.submissions.retrieve(submissionId);
  console.log(sub.student.metadata.canvas_user_id);
  // → push the grade back to Canvas with their own user id
  ```

  ```php PHP theme={null}
  $sub = $hk->submissions->retrieve($submissionId);
  echo $sub->student->metadata->canvas_user_id;
  // → push the grade back to Canvas with their own user id
  ```
</CodeGroup>

## Pitfalls

<Warning>
  **Email is not a good primary key.** Students change emails. Always
  also pass an `external_id` from your LMS — that's what survives email
  changes.
</Warning>

<Warning>
  **Don't sync passwords.** Hawkings doesn't need them — students log in
  through your token flow ([Authentication](/authentication#end-user-authentication)),
  not through Hawkings' own login form.
</Warning>
