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:
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
})),
});
$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),
]);
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:
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);
}
}
$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);
}
}
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:
// 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);
});
// 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();
});
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.
Storing external IDs
Always pass external_id (or a metadata object) when creating users
or cohorts. It lets you:
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
$sub = $hk->submissions->retrieve($submissionId);
echo $sub->student->metadata->canvas_user_id;
// → push the grade back to Canvas with their own user id
Pitfalls
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.
Don’t sync passwords. Hawkings doesn’t need them — students log in
through your token flow (Authentication),
not through Hawkings’ own login form.