API Reference¶
All paths are relative to the base URL (sandbox: https://sandbox.lms.api.fsri.org/api/partner/v1). Every request needs Authorization: Bearer {token} and x-api-key: {{apiKey}} (Authentication) except where noted. Scope: read = partner-api/read, write = partner-api/write.
Machine-readable spec + try-it-out UI: interactive docs.
Quick Reference¶
| Method | Path | Scope | X-User-ID |
|---|---|---|---|
| GET | /ping |
(API key only) | — |
| POST | /users/register |
write | — |
| GET | /users/{uuid} |
read | — |
| PATCH | /users/{uuid} |
write | — |
| DELETE | /users/{uuid} |
write | — |
| GET | /organizations · /organizations/{id} |
read | — |
| GET | /categories · /categories/{id} |
read | — |
| GET | /courses · /courses/{id} |
read | — |
| GET | /modules · /modules/{id} |
read | — |
| GET | /modules/{id}/content |
read | ✔ |
| GET | /modules/{id}/refresh-url |
read | ✔ |
| POST | /modules/{id}/complete |
write | ✔ |
| GET | /course-progresses · /course-progresses/{id} |
read | ✔ |
| GET | /transcripts · /transcripts/{id} |
read | ✔ |
| GET | /transcript/pdf |
read | ✔ |
| GET | /transcripts/{id}/certificate/pdf |
read | ✔ |
Health¶
GET /ping¶
Service health. Requires only the API key (no JWT — by design, so you can verify connectivity in isolation). Returns 200 with "status": "healthy" and dependency checks.
Users¶
POST /users/register¶
Register a learner; returns their profile including their UUID — persist it, it's the learner's identity in every subsequent call. Idempotent per partner+email: registering the same email again returns the existing record, so retries after timeouts are safe.
curl -s -X POST "$BASE_URL/users/register" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "x-api-key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "jane.doe@yourcompany.example",
"firstName": "Jane",
"lastName": "Doe",
"organizationId": 123
}'
Returns 201 with the profile.
Profile fields (response, and PATCH-able): email, firstName, lastName, organizationId, countryCode (ISO 3166-1 alpha-2), address, address2, city, stateProv, postalCode, phone, status (active | inactive) — plus read-only uuid, createdAt, updatedAt.
GET /users/{uuid}¶
Fetch a learner's profile (fields above).
PATCH /users/{uuid}¶
Update profile fields. Requires Content-Type: application/merge-patch+json — send only the fields you're changing:
curl -s -X PATCH "$BASE_URL/users/$USER_UUID" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "x-api-key: $API_KEY" \
-H "Content-Type: application/merge-patch+json" \
-d '{"firstName": "Janet"}'
Deactivate / reactivate — same PATCH with {"status": "inactive"} (or "active"). Deactivation is reversible; while inactive, user-scoped calls with that UUID are rejected. Transcripts, certificates, and progress are preserved.
DELETE /users/{uuid}¶
Permanent GDPR erasure. Returns 204. The learner's identifying data is removed; their transcripts, certificates, and progress records are retained but anonymized (de-identified, relinked to a placeholder) so already-issued credentials remain verifiable. Not reversible. A UUID you don't own returns 404.
Organizations¶
GET /organizations · GET /organizations/{id}¶
Read-only reference data. Use organizations to populate a dropdown at registration; pass the chosen id as organizationId when registering. Managed by FSRI — the API cannot create or modify them.
Catalog: Categories, Courses, Modules¶
GET /categories · /categories/{id} — GET /courses · /courses/{id} — GET /modules · /modules/{id}¶
The content catalog. All three collections support pagination and updatedAt delta-sync filters. Course detail includes its modules.
Only published content is visible through the API.
Module records include passingScore (integer 0–100, or null for non-assessed content) — surface it so learners know the threshold before starting.
Content Delivery¶
GET /modules/{id}/content — user-scoped¶
Returns a signed, time-limited content URL for a learner:
{
"@type": "ModuleContent",
"module_id": 467,
"content_type": "scorm",
"content_url": "https://…/module_0467/index_lms.html?…",
"expires_at": "2026-07-26T15:30:00Z",
"duration_seconds": 600,
"mime_type": "text/html"
}
content_typeis one ofvideo,document,scorm,resource.- Embed
content_urlin an<iframe>(SCORM/documents) or player (video). FSRI hosts and renders SCORM — your app never interprets the package. - The URL is signed and expires (
expires_at, ~60 minutes). Fetch at click-time; never persist a content URL. - Side effect: the learner's progress record is auto-created (status
in_progress) on first content access — you never create progress records yourself.
Status codes: 200 published + user is yours · 400 missing X-User-ID · 404 module not reachable through a published course, or the UUID isn't yours.
GET /modules/{id}/refresh-url — user-scoped¶
Fresh signed URL for content already playing — same response shape as /content. For long-form video, call this when expires_at − now < 60s so playback never breaks mid-session.
POST /modules/{id}/complete — user-scoped¶
Completion notification for video, document, and resource modules — your UI's "Mark as Complete" control calls this. No request body.
curl -s -X POST "$BASE_URL/modules/$MODULE_ID/complete" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "x-api-key: $API_KEY" \
-H "X-User-ID: $USER_UUID"
| Status | Meaning |
|---|---|
202 Accepted |
Completion recorded (FSRI emits the learning-record statement server-side) |
202 (repeat call) |
Idempotent no-op — already complete, safe to retry |
409 Conflict |
Module is SCORM — SCORM reports its own completion; don't call this for SCORM |
400 / 403 / 404 |
Missing/invalid X-User-ID, inactive user, or module/user not found |
Course Progress¶
GET /course-progresses · GET /course-progresses/{id} — user-scoped¶
The learner's progress records. Read-only — records are created on first content access and updated by FSRI as the learner works (SCORM statements or your /complete calls).
Fields include status (not_started | in_progress | completed), completionPercent, lastActivityAt, startedAt, completedAt, createdAt, updatedAt. Course-level completion is derived from the learner's transcript — a course shows completed once FSRI's system of record says so.
Supports updatedAt delta-sync filters — poll on a schedule with those rather than refetching everything.
Transcripts & Certificates¶
GET /transcripts · GET /transcripts/{id} — user-scoped¶
The learner's completion records: course/module reference and name, completionDate, score (0–100 or null), isCourse, certificateAvailable. Supports updatedAt filters.
When certificateAvailable is true, the certificate PDF endpoint will serve it.
GET /transcript/pdf — user-scoped¶
The learner's full transcript (all completions) as application/pdf. Generated on demand, typically 1–3 seconds — call when the user asks for the document; don't pre-fetch in bulk.
GET /transcripts/{id}/certificate/pdf — user-scoped¶
Single course certificate as application/pdf. The transcript must belong to the X-User-ID learner.