Skip to content

Integration Workflows

You own the end-user experience entirely — FSRI publishes no UX requirements, design patterns, or visual styling rules. The flow below is the canonical consumption pattern the API was designed around; deviate where your product needs to, but each step maps to intended endpoint semantics.

The Canonical Flow

Step Your LMS does API call
1 Learner first touches FSA content → register them, store the UUID POST /users/register
2 Build/refresh your catalog (scheduled delta syncs) GET /courses + updatedAt[after]
3 Render course + modules; show passingScore for assessed modules GET /courses/{id}
4 Click-to-play → embed the signed URL per content_type GET /modules/{id}/content
5 Long video: refresh the URL before it expires GET /modules/{id}/refresh-url
6 "Mark as Complete" control (video/doc/resource — not interactive) POST /modules/{id}/complete
7 Show progress in your dashboards GET /course-progresses
8 Transcript shows completed → offer the certificate GET /transcripts/{id}/certificate/pdf

Step by step

  1. Register the learner on their first contact with FSA content; persist the returned uuid alongside your own user record. Registration is idempotent, so re-registering after a lost response is safe.
  2. Sync the catalog — GET /courses (and /categories, /modules) to build your catalog UI. After the first full pull, use updatedAt[after] delta syncs on a schedule (daily is plenty for most partners) rather than re-fetching everything. Never call catalog endpoints in your page-render path — cache on your side.
  3. Render the course — show its modules; for assessable modules surface passingScore so the threshold is visible before starting.
  4. Launch content — on click, GET /modules/{id}/content, then embed content_url in an iframe/player suited to content_type. Embed the URL exactly as returned: its query string carries the parameters the content uses to report progress, so stripping or rewriting it silently breaks completion. FSRI records the access and the progress record becomes in_progress automatically. 4a. Ask for marketing consent once — the first time a learner opens a lesson, if GET /users/{uuid} shows marketingConsent: unset, show the ULRI prompt and record their answer either way. See Marketing Consent.
  5. Keep long content alive — watch expires_at and call refresh-url shortly before expiry so playback never breaks. 5a. Catch the end of the module — see Knowing when a learner finishes so your page can advance them.
  6. Handle completion — see Completion Triggers below.
  7. Reflect progress — poll GET /course-progresses (delta-filtered) to update status and percent-complete displays. Poll proportionately: progress freshness only matters when a human is looking.
  8. Deliver credentials — when a transcript shows status: completed, expose the certificate and full transcript PDFs to the learner.

Completion Triggers by Content Type

Content type How completion reaches FSRI
video / document / resource You call POST /modules/{id}/complete when the learner marks it complete
interactive The package reports its own progress/completion — no partner action (calling /complete returns 409)
Quizzes (inside interactive content) Emitted by the content with pass/fail and score

Who writes progress? Nobody on your side.

Progress records are never written by partners — they're created automatically on first content access and updated by FSRI. This keeps a single source of truth. You just read.

Knowing when a learner finishes

When a learner reaches the end of an interactive module and clicks Next Lesson, the embedded content hands control back to your page by posting a message to the parent frame. Listen for it and advance the learner however your UI works — the completion is already recorded on our side before the message fires.

window.addEventListener('message', (event) => {
  // Always check the origin: it is the content host we returned in content_url.
  if (event.origin !== 'https://partner-courses.sandbox.lms.fsri.cloud') return;
  if (event.data?.type !== 'fsri:module-complete') return;

  const { moduleId, returnUrl } = event.data;
  // Advance the learner: next module, back to the course page, close a modal…
  window.location = returnUrl ?? `/courses/current?after=${moduleId}`;
});

Two things to set up for this:

  • Register your embed origins with FSRI (the scheme and host of each page containing the iframe, e.g. https://lms.yourcompany.com). Messages are delivered only to a registered origin, so completion events can never be read by another site embedding the same content. Without one registered, no message is posted.

    Register every environment you embed from up front, including local development, and all of them keep working. The completion page offers the message to each registered origin in turn and the browser delivers it only to the one that is genuinely embedding, so there is nothing to switch over at go live and no per request parameter to send. A localhost origin is fine to leave registered alongside production. - Optionally pass returnUrl on GET /modules/{id}/content (and refresh-url) — a page on one of your registered origins. It is handed back to you inside the message. FSRI never navigates the learner there itself; your code decides, so a mistyped or foreign URL is inert rather than dangerous.

If you do not implement the listener, nothing breaks: the learner simply lands on a neutral "Module complete" page inside the frame, with their progress already saved.

Reading the content's own traffic while debugging (v3.5)

You'll see the embedded package POSTing statements to /api/partner/v1/xapi/… in the learner's browser network log — that is the content reporting, working as designed; never call those endpoints from your own code. When debugging a run, the status codes distinguish outcomes: 200 means the statement recorded something new; 204 means it was accepted but deliberately changed nothing — an already-completed module, or an interim result. Both are success. A learner replaying a finished module produces a stream of 204s, which is correct, not a fault.

The origin differs per environment

The event.origin to check is the content host from content_url on the tier you are integrating against — sandbox and production differ. Read it from the URL you were given rather than hardcoding one and forgetting the other.

Offboarding

When a learner leaves your platform:

  • PATCH /users/{uuid} with {"status": "inactive"} — reversible; right for departures that might return. Their data stays intact.
  • DELETE /users/{uuid} — permanent GDPR erasure; for right-to-be-forgotten requests. Identity removed; transcripts/certificates/progress retained but anonymized so issued credentials stay verifiable.

Your product's privacy policy decides which; the API supports both.

Sandbox Behavior Worth Designing For

  • Monthly content refresh: sandbox content re-syncs from production monthly — content IDs can shift and test artifacts may be cleaned up. Bootstrap test state at runtime (register fresh users, discover IDs from list endpoints); never hardcode IDs.
  • Only published content is visible — in both sandbox and production.