Unified.to
All articles

How to Sync Enrollment Rosters Across LMS Platforms with Unified's LMS API


February 19, 2026

Enrollment syncing breaks when products assume every LMS exposes the same roster model.

Some platforms provide course-level membership lists. Others expose section-level rosters. Some do not expose enrollment lists at all. Few support webhook notifications for roster changes. And despite generic update payloads, most providers do not allow writing enrollment state through unified APIs.

When teams build enrollment sync logic on top of assumptions instead of documented capabilities, the result is fragile integrations, incomplete rosters, and inconsistent reporting.

Unified's LMS API takes a more explicit approach. It does not invent an Enrollment object. It does not simulate real-time events where none exist. Instead, it exposes enrollment as it exists in the underlying LMS — primarily through student_ids arrays on Course objects (and, where supported, Class objects). Syncing enrollment becomes a disciplined polling and reconciliation process, not a black-box event stream.

This guide shows how to mirror enrollment rosters across LMS platforms using Unified's LMS API — accurately, incrementally, and with full awareness of provider variability.

What 'Sync Enrollments' Means Here

In this context, syncing enrollments means:

  • Reading roster data from LMS platforms
  • Building a canonical enrollment graph in your system
  • Updating your system when enrollment changes occur
  • Enriching rosters with course, content, and progress data

It does not mean:

  • Writing enrollments back into LMS platforms
  • Bi-directional enrollment propagation
  • Real-time webhook-based event syncing

Unified supports read-based roster mirroring. Write support for enrollment state is not broadly available across LMS providers.

The Canonical Enrollment Model in Unified

Unified does not expose a standalone Enrollment object.

Enrollment is represented through relationships:

  • Course.student_ids → array of enrolled student IDs (where supported)
  • Class.course_id → required link to parent Course
  • Class.student_ids → optional and rarely populated across providers

The Student object does not list course memberships. Enrollment must be derived by querying Courses or Classes and inspecting their student_ids arrays.

Activity objects track progress (e.g., is_completed, progress_percentage, started_at, completed_at), but they are not the source of truth for membership.

Step 1: Fetch Courses

Where supported, Courses are the primary roster source.

async function fetchCourses(connectionId: string) {
  const results = [];
  let offset = 0;
  const limit = 100;

  while (true) {
    const page = await sdk.lms.listLmsCourses({
      connectionId,
      limit,
      offset,
      sort: 'updated_at',
      order: 'asc',
      fields: 'id,name,is_active,student_ids,updated_at',
    });

    if (!page || page.length === 0) break;

    results.push(...page);
    offset += limit;
  }

  return results;
}

If student_ids is empty or unsupported for a provider, roster data may not be available.

Step 2: Build the Enrollment Graph

function buildEnrollmentGraph(courses: any[]) {
  const graph: Record<string, string[]> = {};

  for (const course of courses) {
    graph[course.id] = course.student_ids ?? [];
  }

  return graph;
}

This creates:

Course ID → [Student IDs]

Where roster data is unavailable, the graph will be empty for that provider.

Step 3: Handle Provider Variability

Current patterns across providers:

  • Google Classroom → course.student_ids populated
  • LearnUpon → course.student_ids populated
  • Many others → roster fields empty or unavailable
  • class.student_ids → rarely populated across providers

Your sync logic should:

  • Check for student_ids presence
  • Treat missing arrays as 'roster not exposed'
  • Avoid assuming section-level rosters exist

Step 4: Detect Enrollment Changes

Because LMS integrations do not provide webhook notifications for roster changes, polling is required.

Where updated_gte is supported by the provider:

await sdk.lms.listLmsCourses({
  connectionId,
  updated_gte: lastSync,
  sort: 'updated_at',
  order: 'asc',
  fields: 'id,student_ids,updated_at',
});

Where incremental filtering is not supported:

  • Perform periodic full scans
  • Reconcile by id
  • Compare student_ids arrays
  • Deduplicate changes client-side

Step 5: Optional — Enrich with Content

await sdk.lms.listLmsContents({
  connectionId,
  course_id: courseId,
  fields: 'id,name,duration_minutes,is_active',
});

This enables:

  • Enrollment dashboards
  • Course structure display
  • Completion tracking alignment

Step 6: Optional — Attach Progress Data

await sdk.lms.listLmsActivities({
  connectionId,
  course_id: courseId,
  student_id: studentId,
  fields: 'id,is_completed,progress_percentage,completed_at',
});

Important:

  • Activity existence does not imply enrollment.
  • Progress tracking is separate from roster state.
  1. Perform initial full sync
  2. Persist roster snapshot
  3. Poll periodically
  4. Reconcile student_ids
  5. Deduplicate and handle ordering client-side

This provides consistent, provider-aware roster mirroring without relying on unsupported event streams.

Closing Thoughts

Enrollment syncing is not about pushing data between platforms. It is about maintaining a reliable mirror of roster state across LMS systems — within the boundaries each provider exposes.

Unified's LMS API gives you:

  • A normalized roster model
  • Consistent parameter semantics
  • Polling-based synchronization
  • Transparent provider-level variability

When sync logic is built on documented capabilities instead of assumptions, enrollment management becomes predictable — even across heterogeneous LMS ecosystems.

→ Start your 30-day free trial

→ Book a demo

All articles