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¶
- Register the learner on their first contact with FSA content; persist the returned
uuidalongside your own user record. Registration is idempotent, so re-registering after a lost response is safe. - Sync the catalog —
GET /courses(and/categories,/modules) to build your catalog UI. After the first full pull, useupdatedAt[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. - Render the course — show its modules; for assessable modules surface
passingScoreso the threshold is visible before starting. - Launch content — on click,
GET /modules/{id}/content, then embedcontent_urlin an iframe/player suited tocontent_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 becomesin_progressautomatically. 4a. Ask for marketing consent once — the first time a learner opens a lesson, ifGET /users/{uuid}showsmarketingConsent: unset, show the ULRI prompt and record their answer either way. See Marketing Consent. - Keep long content alive — watch
expires_atand callrefresh-urlshortly 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. - Handle completion — see Completion Triggers below.
- 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. - 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
localhostorigin is fine to leave registered alongside production. - Optionally passreturnUrlonGET /modules/{id}/content(andrefresh-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.