JAVASCRIPT SDK
JavaScript SDK Reference
Add a course player to your LMS or website with one JavaScript file. Play a course created through the HTTP API using its course_id, including an externally hosted AICC course or LTI activity, or pass a package URL as fetch_url and let SCORMBox download and prepare it the first time it opens. Both ways save learner progress and return completion, pass/fail, and scores reported by the content. Supported formats are SCORM 1.2, SCORM 2004, AICC, H5P, TinCan, ADL xAPI Launch, cmi5, LTI 1.1, LTI 1.3, MP4/WebM video, PDF, and ordinary HTML5 ZIPs.
Packages can be up to 500 MB, with up to 2 GB extracted, 500 MB per file and 5,000 ZIP entries. SDK projects belong to confirmed email owners. The player shows download and unpacking progress before the course opens. For supported formats and sharing help, see the FAQ. For uploads, signed sessions and status reads from any server language, see the HTTP API Reference and six uploader examples.
What’s the use case?
Building an LMS or adding training to your app? Play SCORM, AICC, LTI, H5P, xAPI and cmi5 courses through one integration, instead of implementing each standard yourself.
Upload or connect a course, choose a learner, and open the player in your app. Your JavaScript receives saved progress and result updates through SDK events. Read completion, pass/fail, scores and time in the same normalized_details shape across formats, with native data in runtime_details. Your server can retrieve the same saved results through the HTTP API. Available values depend on what the course reports.
eLearning standards shouldn’t be this difficult. You build the LMS. We handle course playback and reporting. Try the live demo and copy the generated integration code.
Playing your course
In the API, launch means opening a course for a learner. Call player.launchCourse() to play it. The returned object is named launch in the examples; use it to read saved progress, receive events and control playback.
Open Manage keys, confirm your email, and create a project with the exact website origins of your LMS. Include a port when applicable. HTTPS origins work in production; http://localhost:3000 and http://127.0.0.1:3000 work for development. Register every ancestor origin when embedding inside another iframe.
Website access rules
An exact origin includes its scheme, hostname and port, such as https://lms.example.com:8443. Leave out page paths, queries and fragments. Website access applies to playback and browser result lookups across the project, including after registration-key rotation. A revoked registration key cannot launch courses.
Only superadmins can enable wildcard subdomains or any-website access. Project owners can keep a staff-managed policy while editing the project name, or replace it with a narrower selection of exact websites. Owners cannot enable or change broad access rules through the dashboard or project API.
| Rule | Websites allowed |
|---|---|
https://lms.example.com | That exact HTTPS website on its default port. Other subdomains and ports are excluded. |
https://*.example.com:8443 | HTTPS subdomains such as https://team.example.com:8443 and https://training.team.example.com:8443. The apex https://example.com:8443 is excluded unless it is also registered exactly. The scheme and port must match. |
* | Any HTTPS website, including custom domains and non-default ports, plus HTTP localhost and 127.0.0.1 development addresses. Other HTTP websites and non-web origins are excluded. |
The superadmin subdomain option retains each exact base website and adds matching HTTPS subdomains. Localhost and IP addresses stay exact. Any-website access retains the selected websites so turning it off can restore the restricted selection. A broad rule grants website access; your application still chooses and authenticates its learners. Signed sessions always bind a token to one concrete allowed origin, never a wildcard or *; a token issued for one tenant website cannot be replayed on another.
For a SaaS app with several tenants, see Identify a learner and use tenant-prefixed learner IDs with signed server sessions.
Choose your course source
With a registration key, there are two ways to provide the course to player.launchCourse():
| Course source | What to pass | What SCORMBox does |
|---|---|---|
| Create the course through the HTTP API: upload a file, import a package URL, add an AICC launch URL, or register an LTI activity. | course_id returned by the HTTP API. | Plays that existing course from the same SDK project. See the course ID example, direct AICC example, or LTI example. |
| Give the SDK a downloadable package URL. No separate upload request is needed. | fetch_url, such as an HTTPS ZIP URL on your storage server. | Downloads and unpacks it the first time it opens, then reuses the prepared course. See the URL example. |
Choose one: course_id or fetch_url. The course source belongs in launchCourse() options. ScormBoxPlayer.initialize() takes your registration key or signed launch token; it does not take the package URL. Both source options use the same learner, resume, display, event and result options.
fetch_url must return a downloadable ZIP or .h5p package. For a provider-hosted AICC launch URL or an LTI activity, create the course through the HTTP API first and pass its course_id. launchCourse() does not accept launch_url directly.
Play from a package URL
Include the script and create a player with your registration key, then pass fetch_url to launchCourse(). The URL must remain downloadable while SCORMBox prepares the package; temporary signed URLs work too.
<script src="https://scormbox.com/sdk.min.js"></script>
<div id="training"></div>
<script>
const player = ScormBoxPlayer.initialize("YOUR_REGISTRATION_KEY");
const launch = player.launchCourse({
el: "#training",
fetch_url: "https://your-storage.com/training-v1.zip",
// Optional: a stable identity when the download URL changes.
canonical_url: "https://your-storage.com/training-v1.zip",
user: { name: "John Doe", email: "john@example.com" },
height: 720
});
launch.onEvent((type, result) => {
if (type === "state" || type === "ready" || type === "progress" || type === "closed") {
console.log(result.normalized_details, result.runtime_details);
// Send the result to your own application here.
}
});
launch.on("passed", result => console.log("Passed", result.normalized_details));
launch.on("error", error => console.error(error.message));
launch.state.then(result => console.log("Previously saved", result.normalized_details))
.catch(error => console.error("Unable to read initial state", error.message));
launch.ready.then(result => console.log("Opened", result))
.catch(error => console.error("Unable to open", error.message));
</script>launchCourse() returns immediately. With fetch_url, SCORMBox downloads and prepares the archive in the background if that course has not been imported into your project yet. Later openings reuse the prepared course. By default, the embed container shows downloading and unpacking status, with file progress when available, then automatically opens the player. Downloads use an indeterminate indicator rather than a guessed percentage. Subscribe immediately; state returns the saved snapshot before the course frame opens, including normalized status and runtime details. ready resolves after the player frame opens and saved results can be read. It does not guarantee the content's own internal initialization or external resources have succeeded.
Download an updated package
SCORMBox normally reuses the prepared course for a canonical URL. To download changed content at that same URL, pass reprocess: "force" (or true) to launchCourse():
const launch = player.launchCourse({
el: "#training",
fetch_url: "https://your-storage.com/training.zip?YOUR_FRESH_SIGNATURE",
canonical_url: "https://your-storage.com/training.zip",
reprocess: "force",
user: { name: "John Doe", email: "john@example.com" }
});
launch.ready.then(result => {
console.log(result.course_id, result.content_revision, result.previous_course_id);
});This prepares a new content version with a new course_id and starts the learner fresh. Previous versions, attempts and results stay available under their original course IDs. Open courses keep using their original files. The canonical URL points to the new version only after preparation succeeds; a failed refresh leaves the previous version in place.
Provide a fresh fetch_url, optionally with a stable canonical_url. Do not combine reprocessing with course_id, attempt_id or attempt_mode: "reset". Omit reprocess or pass false to reuse prepared content. attempt_mode: "new" starts a new attempt on unchanged content; reprocessing downloads the content again.
One playback handle refreshes once. Reopening its popup or overlay resumes the new course and attempt, even if the window was closed during preparation. Button modes expose a blank planned state with pending_reprocess: true before opening; they do not download until clicked. A separate forced call while the same course is being prepared returns 409. To request another refresh after a failed one, make a new launchCourse() call.
content_revision starts at 1 and increases for each successfully replaced canonical version. previous_course_id identifies the version being replaced, or is null on the first import. These fields appear in preparation metadata and saved results. An explicit old course_id still plays or reads the old version. An explicit old attempt_id with its canonical URL also selects that attempt's original version; an ordinary canonical lookup uses the current version. Retained versions count toward the project's 500-course limit.
Signed browser tokens cannot refresh content. Use POST /courses with reprocess, wait for the new course to be ready, then have your server issue a session for its new course_id.
Try the Live Demo: choose from the downloadable courses in the sample catalog, or enter your own URL. Download again and start fresh exercises reprocessing. Sample links include source credits; they are third-party test courses, not content you own.
Play an uploaded course
Create the course first using the HTTP API. Send fetch_url and optional canonical_url to import from S3 or another storage URL, or transfer a local file directly or with a sample uploader. After preparation completes, pass the returned course_id to the browser SDK. Use the same project’s browser registration key. You can create server API credentials in Manage keys. Courses belong to the project’s confirmed email owner. New API courses default to SDK-only access; opt into learner links with shareable: true.
const launch = player.launchCourse({
el: "#training",
course_id: "COURSE_ID_FROM_UPLOAD",
user: { name: "John Doe", email: "john@example.com" }
});
launch.onEvent((type, result) => console.log(type, result.normalized_details));
launch.state.then(result => console.log(result.runtime_details));Supply either course_id or fetch_url, without mixing selectors. Unknown IDs return an error without downloading or creating another course. Playing by course ID supports the same resume/new/reset options, display modes and events. Unfinished uploads show a waiting message and extraction progress. Keep API secrets on your server; they are never SDK initialization keys. See the HTTP API Reference for multipart uploads up to 500 MB, retry handling, sharing, signed sessions and GET results. Download standalone uploaders in Ruby, Python, JavaScript, PHP, C# and Java.
Identify a learner
Pass user.name and either user.id or user.email. A student ID lets your LMS identify learners without collecting an email address:
const user = { name: "John Doe", id: "student-123" };
const launch = player.launchCourse({ el: "#course", course_id: "YOUR_COURSE_ID", user });
const result = await player.getResult({ course_id: "YOUR_COURSE_ID", user });
// result.user: { name: "John Doe", id: "student-123", email: null }IDs are strings of 1–200 UTF-8 bytes. Surrounding whitespace is removed; case is preserved and significant. Keep the same ID to resume progress. IDs belong to your SDK project and remain separate from email identities, even when an ID looks like an email address. Supply one identity field, without combining id and email. IDs do not grant email confirmation, My courses, management access or access to another learner's progress. Your LMS remains responsible for authentication.
Use the same user object for playback, results, report viewers, attempt history and server-created signed sessions. The shared learner-link UI continues to use name and email.
Multiple SaaS tenants in one project share a learner identity namespace. Different websites do not isolate the same user.id or email into different learners. Prefix IDs with a stable tenant identifier, for example tenant-acme:student-123 and tenant-beta:student-123, and use that full value consistently for launches and results. An email shared across tenants still identifies the same learner in that project; use tenant-prefixed IDs when separate progress is required. For trusted identity, authenticate the learner on your server, select the tenant-prefixed ID there, and issue a signed session for the tenant’s concrete website origin. A browser-supplied ID alone is not authentication.
Play an AICC URL
On your server, create an AICC URL course with POST /api/v1/courses and { format: "aicc", launch_url: "https://provider.com/course/start" }. Optional properties include title, shareable and a stable canonical_url. The response is ready immediately with a course_id, source_type: "aicc_url" and upload: null; SCORMBox does not download or unpack the provider's content.
Use the returned ID in your browser with the same project's registration key:
const player = ScormBoxPlayer.initialize("YOUR_REGISTRATION_KEY");
const launch = player.launchCourse({
el: "#training",
course_id: "COURSE_ID_FROM_AICC_URL_CREATION",
user: { name: "John Doe", email: "john@example.com" },
attempt_mode: "resume",
display_mode: "iframe"
});
launch.onEvent((type, result) => console.log(type, result.normalized_details));
launch.state.then(result => console.log("Saved progress", result.runtime_details));
launch.ready.catch(error => console.error(error.message));The existing learner, resume/new/reset, event and report methods apply. Read or show results with the same course_id and learner ID or email using getResult() or showReport(). You can also authorize playback with a signed session. Omit runtime for AICC. Direct URL courses do not accept forced reprocess; the content stays on the provider's server.
The URL must launch an actual AICC HACP course. SCORMBox preserves its URL parameters and supplies the active AICC_SID and AICC_URL. The HACP receiver supports browser and provider-server callbacks with saved completion, score, time and bookmarks. An ordinary webpage gains no tracking from this flow; availability and reported results depend on the provider.
If the provider blocks iframe embedding, choose display_mode: "popup" or "new_window", or use the player's Open course in new tab fallback. The provider's Finish/Exit control sends its final report; SCORMBox cannot force external content to report a save.
Play an LTI tool
SCORMBox launches external tools as an LTI 1.1 Tool Consumer or LTI 1.3 Platform. Set up the tool in either of two ways:
- Through HTTP: your server calls POST /api/v1/lti/connections to register the tool, then POST /api/v1/courses with format: "lti" and its connection_id to add an activity. Follow the complete Add an LTI tool via HTTP example, with environment variables and a downloadable Node.js file. - Through the website: a verified project owner opens LTI tools, chooses a project, and enters the provider settings.
For LTI 1.1, use the provider’s consumer key and shared secret. For LTI 1.3, use its login initiation URL, redirect URIs, and public signing keys, then register SCORMBox’s returned platform settings with the provider. Secrets and private keys stay on the server. The browser SDK uses the ready course_id; it does not create a tool connection or accept provider secrets in launchCourse().
Add a course using an activity launch URL, or use Choose activities in tool for Deep Linking 2.0. Review the returned activities, choose one and explicitly add its course. Server integrations can register connections and create LTI courses through HTTP. A connection can serve several courses in its project.
The ready course returns a course_id. Use it with the same project’s browser registration key:
const player = ScormBoxPlayer.initialize("YOUR_REGISTRATION_KEY");
const learnerCourse = {
course_id: "COURSE_ID_FROM_LTI_CREATION",
user: { name: "John Doe", email: "john@example.com" }
};
const launch = player.launchCourse({
...learnerCourse,
el: "#training",
display_mode: "iframe",
attempt_mode: "resume"
});
launch.onEvent((type, result) => console.log(type, result.normalized_details));
launch.ready.catch(error => console.error(error.message));
// Read again later if the provider grades asynchronously.
const saved = await player.getResult(learnerCourse);
console.log(saved.normalized_details, saved.runtime_details);If the provider refuses iframe embedding, SDK popup or new_window display modes may help; use the player’s provider-opening fallback when the provider still blocks its embedded frame.
LTI course formats are lti11 and lti13. Pass course_id rather than fetch_url, launch_url or provider credentials. LTI courses are ready without downloading packages, and do not support archive downloads or forced reprocess. Learner sharing is optional; use the separate learner link from the course’s management page when enabled.
For server-authorized playback, call POST /api/v1/courses/{course_id}/sessions with the learner, registered origin and optional attempt selection. Give the browser only launch_token, then use ScormBoxPlayer.initialize({ launch_token }) and launchCourse({ el: "#training" }). The signed playback walkthrough applies unchanged; no LTI secret belongs in browser code or SDK events.
Resume preserves stable learner, context and resource-link identifiers. The external tool controls its own bookmarks and saved work. Local new or reset separates SCORMBox attempts/results but cannot universally reset the provider’s activity. Reset archives the previous generation; a grade targeting that generation cannot overwrite the new one. Previews never write learner submissions. Disabling the project or connection blocks launches and grade updates while retaining stored results.
LTI 1.3 AGS supports score posting and result reads, without roster access or gradebook/line-item management. activityProgress: "Completed" sets completion; gradingProgress: "Failed" is a grading-process error, not learner failure. Pass/fail stays unknown unless a configured passing_score percentage and a final numeric grade permit deriving it. LTI 1.1 Basic Outcomes reports scores only. Closing a window does not mark completion or force a final grade. Valid asynchronous grades may arrive after closure, so read saved results again when the provider finishes grading.
Authorize playback from your server
To let your LMS authorize playback, your server authenticates the learner and calls the HTTP API to obtain a signed launch token (launch_token). This token permits playback for one course, learner and attempt. Keep the API key and secret on the server. The browser uses only the token:
const player = ScormBoxPlayer.initialize({ launch_token: session.launch_token });
const launch = player.launchCourse({ el: "#training", display_mode: "iframe" });
launch.ready.catch(error => console.error(error.message));
const saved = await player.getResult();A signed launch token already supplies the course, learner and attempt. Omit course_id and fetch_url in this flow. Playback resumes the authorized attempt; it cannot create or reset an attempt.
Preview a course from your server
For an administrator or author preview, have your server call POST /courses/{course_id}/previews after checking that person's permission to view the course. Pass only the returned preview_token to the browser:
const player = ScormBoxPlayer.initialize({ preview_token: preview.preview_token });
const launch = player.launchCourse({ el: "#training", display_mode: "iframe" });
launch.onEvent((type, result) => {
// Preview events describe this temporary preview only.
console.log(type, result.preview);
});
await launch.ready;
const current = await launch.getDetails();
await launch.close();
await launch.destroy();The token supplies one course, temporary session_id, website origin and playback selection. Omit learner and attempt options. The same iframe, popup, new window and overlay display modes and launch handle methods (state, ready, onEvent, getResult, getDetails, close, destroy) apply. Open popup and new window buttons from a direct user click. Preview result objects have preview: true, has_attempt: false and attempt_id: null. They never create or update learner attempts, scores, completion or enrollment time.
launch.getDetails() reads the current preview, including live SCORM results while its player is open. SCORM preview results are temporary and disappear when that player is removed; player.getResult() cannot recover them from the service. Other formats keep temporary results for this session only. HTML5 previews track viewing time automatically. After close, launch.getResult() retains the final result in memory. getAttemptHistory() and showReport() apply to learner records; use the preview launch handle to inspect temporary results.
The token must be used within five minutes. An open preview lasts up to 24 hours and stops when closed or when its credential or website access is revoked. A closed preview requires a fresh server authorization. Never use preview events to update learner records; use a learner session and an authenticated server result read for that purpose.
Options
These options apply to initialize() and launchCourse(). The separate public embedCourse() helper uses the shared-course options above.
ScormBoxPlayer.initialize(key) selects the service URL from the included SDK script. For a custom service or local development, pass an optional settings object: ScormBoxPlayer.initialize(key, { base_url: "http://localhost:8787" }). Production service URLs must use HTTPS. For server-authorized playback, use ScormBoxPlayer.initialize({ launch_token }); for a custom service, pass settings as the second argument: ScormBoxPlayer.initialize({ launch_token }, { base_url: "http://localhost:8787" }). The token supplies the authorized course, learner and attempt.
| Option | Meaning |
|---|---|
el | CSS selector or DOM element. Receives the inline player, or an Open course button for the other display modes. |
display_mode | "iframe" (default), "popup", "new_window" or "overlay". |
button_text | Optional text for the Open course or View results button. |
window_width, window_height | Browser popup dimensions in pixels; default 1100 × 800. The browser can adjust these. |
fetch_url | Public HTTPS ZIP or .h5p download URL. Signed URLs are supported. No browser-to-storage CORS configuration is needed for the download. |
course_id | Alternative to fetch_url: play a course already created in this project through the server API, including a direct AICC URL course or LTI activity. Do not combine with fetch_url or canonical_url. |
canonical_url | Optional stable HTTPS course identity. It does not have to be downloadable. Defaults to a normalized fetch URL. |
reprocess | Omit or false to reuse prepared content. true or "force" downloads a fresh content version; requires fetch_url and starts fresh. Details. |
user.name | Required learner name. |
user.id or user.email | Supply either a case-sensitive student ID or an email address. Email is trimmed and lowercased. See Identify a learner. |
attempt_mode | "resume" (default), "new" or "reset". See attempts below. |
attempt_id | Optional stored attempt ID for resume; required for reset. This is separate from the playback session_id. |
height | Inline iframe height in pixels or a CSS string; defaults to 640px. |
title | Optional accessible iframe title. |
show_loading | Defaults to true. Set false to render your own preparation UI from importing events. |
show_header | Defaults to true. Set false for an iframe to hide its title, status and close bar when your page provides playback controls. PDF and video use a compact viewer. Error messages remain visible. Popup, new-window and overlay players keep their header. |
Methods use camelCase, such as launchCourse(), getResult() and onEvent(). All SDK options, result properties and event payload properties use snake_case, such as display_mode, attempt_id, course_id, session_id and save_confirmed. Pass returned IDs directly: attempt_id: result.attempt_id and course_id: result.course_id. CamelCase option names are rejected; there are no aliases or automatic name conversion. Native format data in runtime_details.data and xAPI statements retain their original schemas.
The same project, canonical course and learner ID or email identify a learner’s attempts. Resume selects the latest attempt for the playback selection, even if completed, unless you specify an attempt_id. Keep the returned attempt_id to reopen that exact attempt. A different project has separate content and learners. Opening the same learner’s attempt again replaces its older playback session. Previews from My courses do not count as learner submissions.
Known S3 virtual-host and path-style endpoint URLs normalize to the same identity. Recognized AWS, Google and CloudFront temporary signing parameters are removed from that identity; version IDs and ordinary query parameters are preserved. The original fetch URL, with its signature, is used for downloading. For unrelated signed URL schemes, supply canonical_url explicitly. Canonical URLs may appear in results and your private dashboard, so keep secrets out of them.
The first successful import is reused. Replacing a ZIP at the same canonical URL does not refresh the cached content. Use a versioned path or query value for a new content version. If an import fails, delete it from My courses before retrying, or use a new canonical identity.
This JavaScript example reads saved results before playing the course:
const player = ScormBoxPlayer.initialize("YOUR_REGISTRATION_KEY");
const courseOptions = {
course_id: "COURSE_ID_FROM_UPLOAD",
user: { name: "John Doe", email: "john@example.com" }
};
function playCourse() {
return player.getResult(courseOptions).then(saved => {
console.log(saved.normalized_details);
const launch = player.launchCourse(Object.assign({}, courseOptions, { el: "#training" }));
return launch.ready.then(() => launch);
});
}
playCourse().catch(error => console.error(error.message));Resume, new attempts and reset
The hierarchy is SDK project → canonical course → learner ID or email → attempt → playback session → activity state. LTI new/reset isolates local results; the external tool may retain its own work. An attempt_id persists across openings. Each opening gets a fresh session_id; do not pass a session ID as an attempt ID. Attempt numbers increase within the course and learner. Resuming an older attempt does not make it the newest attempt.
| Playback options | Behavior |
|---|---|
Omit both attempt options, or attempt_mode: "resume" | Resume the latest attempt for the playback selection. Create the first if none exists. |
attempt_mode: "resume", attempt_id | Resume that existing attempt for this project, course and learner. An unknown or mismatched ID returns an error. |
attempt_mode: "new" | Create a fresh ID with empty progress. Keep every previous attempt. |
attempt_mode: "reset", attempt_id | Archive this attempt’s current results, then clear its active progress while keeping its ID. |
new with an attempt_id is rejected: use reset to clear an existing ID. Reset requires an existing ID. A reset clears that attempt’s bookmark, saved activity data, completion, success, score and accumulated time. It increments generation, retains a read-only snapshot of the previous generation, and expires prior playback sessions for that attempt. Old tabs and old xAPI credentials cannot restore progress.
const options = {
el: "#training",
fetch_url: "https://your-storage.com/training-v1.zip",
user: { name: "John Doe", email: "john@example.com" }
};
// Default: resume latest.
const launch = player.launchCourse(options);
launch.state.then(result => {
console.log(result.attempt_id, result.session_id, result.generation);
// Store result.attempt_id in your LMS if you want to select it later.
});
// After closing the previous player, choose one of these:
// player.launchCourse({ ...options, attempt_mode: "new" });
// player.launchCourse({ ...options, attempt_id: savedAttemptId });
// player.launchCourse({ ...options, attempt_mode: "reset", attempt_id: savedAttemptId });All modes work across supported formats and display modes. Window and overlay buttons apply new or reset when the course first opens, then resume that returned attempt when reopened. Call launchCourse() again to request another new attempt or reset. Merely creating a button does not create or reset an attempt. A new-attempt button's initial state has pending_attempt: true, empty progress and no attempt ID until it opens. A reset button initially shows the current saved results; the reset occurs when it opens. Requests retried within the same opening reuse one playback session and do not reset twice.
Results contain attempt_number, generation, archived and reset_at in addition to the IDs. player.getResult() and player.showReport() read the latest attempt for the playback selection by default. Supply the saved attempt_id to select an existing attempt.
const lookup = {
course_id: savedCourseId,
attempt_id: savedAttemptId,
user: { email: "john@example.com" }
};
const current = await player.getResult(lookup);
const history = await player.getAttemptHistory(lookup);
// { attempt_id, current_generation, history: [...], page: 0, has_more: false }
// Each history entry has generation, completion, success, score, seconds,
// name, format, started_at, updated_at and reset_at.
if (history.history.length) {
const generation = history.history[0].generation;
const previous = await player.getResult({ ...lookup, generation });
const report = player.showReport({ el: "#results", ...lookup, generation });
await report.ready;
}History contains 25 reset snapshots per page for the selected attempt, newest first; pass page: 1 for the next page when has_more is true. Reading current results or history never changes playback. Deleting a course removes its attempts and history.
Open in a popup
Use display_mode to choose where the course opens:
- "iframe": plays inline in your div. This is the default. - "popup": your div becomes a button that opens a sized browser window using window.open(). - "new_window": your div becomes a button that opens a normal browser tab or window, according to browser preferences. - "overlay": your div becomes a button that opens a modal on the same page.
For the button modes, the SDK reads the existing saved state immediately; downloading and playback begin when the learner opens the course. The same resume and result callbacks work in every mode. In browser window modes, download and unpacking progress appear in the new window.
<script src="https://scormbox.com/sdk.min.js"></script>
<div id="open-training"></div>
<script>
const player = ScormBoxPlayer.initialize("YOUR_REGISTRATION_KEY");
const launch = player.launchCourse({
el: "#open-training",
display_mode: "popup",
button_text: "Open training",
fetch_url: "https://your-storage.com/training-v1.zip",
user: { name: "John Doe", email: "john@example.com" }
});
launch.onEvent((type, result) => {
if (["state", "ready", "progress", "closed"].includes(type)) {
console.log(result.normalized_details, result.runtime_details);
}
});
launch.on("error", error => console.error(error.message));
launch.ready.catch(error => console.error(error.message));
// Your own button can also call launch.open().
</script>The supplied button opens the browser window directly from its click handler. If using your own button, call launch.open() directly in that click handler, before awaiting other work. A blocked popup emits an error with code: "popup_blocked" and does not download or play content; allow popups and click again. Host pages using Cross-Origin-Opener-Policy must allow communication with the SCORMBox window, for example using same-origin-allow-popups rather than same-origin. An enclosing sandbox must allow popups too. See the browser's popup requirements and opener policy documentation.
Save & close waits for the final save before closing the course window or overlay. A failed save leaves it open for a retry. Using the browser's × button cannot guarantee a final save: the SDK returns the last stored result in a closed event with save_confirmed: false and close_reason: "window_closed". A successfully acknowledged window close has save_confirmed: true and close_reason: "saved". Completion and pass/fail always reflect stored course reports. For LTI, closing returns save_confirmed: false and close_reason: "lti_closed": the provider cannot be forced to save or grade, and valid grades can arrive asynchronously afterward.
Closing during preparation cancels that opening; an already-started package import can finish in the background. Reopening creates a fresh session and resumes the attempt selected or created by this playback handle. launch.open() returns a Promise for that opening; launch.ready settles once for the initial opening and rejects if preparation fails. Blocking or cancelling a window before playback allows the initial ready Promise to resolve on a later opening. Subscribe to ready for subsequent openings. The overlay has keyboard focus handling; Escape and backdrop clicks use the acknowledged close flow. It fills the available viewport. Legacy popup: true still selects the overlay; use display_mode for new integrations.
Reports accept the same display_mode, button_text and popup dimensions. Use player.showReport({ el, display_mode: "popup", canonical_url, user: { email } }) to create a View results button. report.open() opens it programmatically; report.close() closes the report while retaining its button. Reopening reads fresh stored results. Closing a report never saves, starts or closes course playback.
Results
Saved result callbacks contain course_id, attempt_id, attempt_number, generation, archived, reset_at, session_id, canonical_url, content_revision, previous_course_id, title, format, user, normalized_details and runtime_details. Events also include type, launch_id and timestamp.
{
normalized_details: {
completion: "completed", // incomplete | completed
success: "passed", // unknown | passed | failed
score: 85, // percentage 0–100, or null
seconds: 120, // saved total duration
modules: [
{ id: "lesson-1", title: "Lesson one",
completion: "completed", success: "passed", score: 85, seconds: 120 }
],
has_report: true,
updated_at: "2026-10-02T12:00:00.000Z"
},
runtime_details: {
format: "1.2",
data: { /* format-specific saved data */ }
}
}Completion and success are independent. A course reporting only completion has success: "unknown". A zero score is 0; missing score is null. The SDK does not infer pass/fail from a numeric score. Time uses the course’s reported time, with an elapsed-time fallback where supported; it is not proof of attention or attendance.
All supported formats return the same normalized_details shape, so completion, success, score, seconds and modules can be read consistently. runtime_details.data contains the course’s saved activity data in its original format.
modules contains SCORM activities, AICC lessons or xAPI/cmi5 launch units when available. H5P and LTI expose their overall saved result with an empty module list. runtime_details.data preserves the course’s saved data.
For convenience, the same common fields also exist as completion, success, score, seconds, units, has_report and updated_at at the top level. Raw snapshots are cached between unchanged saves. getDetails() requests a fresh saved snapshot, including the raw data.
Read status and show a report
Read a learner’s saved results when your page loads, without opening the course. Use the same course and learner ID or email as playback, or supply an existing attempt_id. Use a course_id for a course uploaded through the HTTP API.
<script src="https://scormbox.com/sdk.min.js"></script>
<div id="course-results"></div>
<script>
const player = ScormBoxPlayer.initialize("YOUR_REGISTRATION_KEY");
const learnerCourse = {
canonical_url: "https://your-storage.com/training-v1.zip",
user: { email: "john@example.com" }
};
// Read this on page load, even when you do not open a player.
player.getResult(learnerCourse)
.then(saved => console.log(saved.normalized_details, saved.runtime_details))
.catch(error => console.error(error.message));
const report = player.showReport({ ...learnerCourse, el: "#course-results" });
report.on("error", error => console.error(error.message));
report.ready.then(saved => console.log("Report loaded", saved.normalized_details))
.catch(error => console.error(error.message));
report.on("refreshed", saved => console.log("Latest results", saved.normalized_details));
</script>The compact report shows learner identity, completion, separate pass/fail, score, saved time, last save and lesson results when available. It adjusts its height automatically; supply height in pixels or as a CSS string for a fixed-height frame. title sets its accessible iframe title. The Refresh button reads current stored results. Reports do not poll continuously; call report.refresh() after a player saves or closes if both appear on the same page.
| Method or property | Result |
|---|---|
player.getResult(options) | A Promise for current saved normalized and activity details. Registration-key mode requires user.id or user.email and one of course_id, canonical_url or fetch_url. Signed mode uses its bound learner and attempt. No container is needed. |
player.getAttemptHistory(options) | A Promise for archived reset generations of attempt_id. Uses the same course and learner options, plus optional page. |
player.showReport(options) | Returns a read-only viewer immediately. Uses the same learner options, plus el and optional height, title, display_mode, button_text, window_width and window_height. |
report.ready | A Promise for the initial saved result once the viewer is available. |
report.getResult() | Returns the viewer's latest cached result, or null before loading. |
report.refresh() | Reads current stored results, updates the viewer and returns a Promise for that result. |
report.onEvent(callback), report.on(type, callback) | Subscribe to ready, refreshed and error. Returns an unsubscribe function. Reports do not replay course-completion events. |
report.destroy() | Removes the viewer and its listeners. player.destroy() also removes its reports. |
Reading results never downloads a package, creates an attempt, changes a learner’s name, or replaces an active playback session. A learner with no attempt has has_attempt: false, attempt_id: null, unknown success, a null score and no saved activity data. course_status: "not_imported" means this canonical course has not been imported into the project; this returns an empty result rather than starting an import. An unknown or inaccessible course_id returns an error. Read-only results have session_id: null and expose no playback capability. They are restricted to the registration key’s project and registered calling origin, with the same integration-supplied learner identity as playback.
Events
| Event | Trigger |
|---|---|
importing | Preparation status (fetching, extracting, ready or failed), including cached imports. preparation.completed_files and preparation.total_files report unpacking progress. |
launching | The SDK creates its player iframe. |
state | The saved normalized result and runtime details are available before the course frame opens. This is the initial stored snapshot, including any earlier completion/pass/fail. |
ready | Initial saved result is available. Inspect it for resumed completion/pass/fail. |
progress | Persisted progress changes. |
completed, passed, failed | The corresponding saved status changes. |
score_changed | Saved score changes, including zero. |
module_completed, module_passed, module_failed | A saved unit changes status; payload includes module. |
closed | Player closure is acknowledged and the final saved snapshot is read. |
error | Preparation, opening, saving progress or closing fails. terminal: true means the session cannot continue. |
Events reflect polled saved results, normally within a few seconds. They are status transitions, not an exhaustive stream of every runtime call or statement. Resume returns stored status in state and current status in ready; it does not replay earlier completion/pass events. These browser callbacks are not durable webhooks. Your LMS should process state, ready and closed as well as changes and deduplicate using the attempt and saved result.
HTML5 courses
Upload a ZIP containing index.html and its assets, or pass its ZIP URL as fetch_url. No manifest is needed. Use a root index.html, or put it inside one clearly identified folder. Existing recognized course descriptors take priority. New ZIPs without a descriptor use HTML5; existing xAPI courses keep their current playback settings.
Inside the course, ScormBox is available automatically before your scripts run. No script tag, initialization, learner credentials or key is needed in the content. This is the course author's API; the embedding website continues to use ScormBoxPlayer.launchCourse().
ScormBox.setScore(85);
ScormBox.setStatus('passed');
// Optional: remember your own course data.
ScormBox.setData({ page: 3, answers: ['a', 'c'] });
const saved = await ScormBox.getData();| Method | Behavior |
|---|---|
setStatus(status) | incomplete, completed, passed or failed. Passed and failed also mark completion. The latest explicit status replaces the previous status. |
setScore(score) | A number from 0 to 100. A score alone does not complete or pass the course. |
setData(value) | Replace saved custom data with JSON: an object, array, string, number, boolean or null. Total saved progress is limited to 1 MB. |
getData() | Returns a promise for a copy of the saved custom data, or null on the first visit. |
Setters save automatically and return promises. Ordinary calls may omit await; await a setter before navigating away when you need confirmation that it was saved. Invalid arguments throw an error. Connection errors appear in the course player with a retry button, and an awaited setter rejects if its save cannot be confirmed. Use Save & close to confirm the final save.
const saved = await ScormBox.getData();
const page = saved?.page || 1;
async function goToNextPage() {
await ScormBox.setData({ page: page + 1 });
location.href = 'next.html';
}HTML5 course pages are isolated from parent-page scripts and browser storage such as localStorage and cookies. Use setData/getData for saved progress. Status, score and custom data resume with the learner's attempt, including across local HTML pages. Existing resume, new and reset options apply. Visible viewing time is tracked automatically; time does not prove attention. Results use format: "html5" and runtime: null. normalized_details contains completion, success, score and seconds; runtime_details.data.html5 contains the saved status, score, seconds and custom data. Browser SDK events and HTTP result reads use the same flow as other courses. HTML5 courses can also use connected SCORM or AICC exports.
Download the complete HTML5 example ZIP, upload it, and choose Play preview. For HTTP uploads or a remote ZIP import, see HTML5 courses in the HTTP API.
Videos, PDFs and viewing results
Use the same course_id flow for an uploaded video or PDF. You can also pass a direct HTTPS fetch_url; add format: "pdf" or format: "video" for URLs without a recognizable file extension. canonical_url identifies the course independently of expiring download signatures. These formats use their own viewer and return runtime: null; omit the SCORM runtime option.
const player = ScormBoxPlayer.initialize("YOUR_REGISTRATION_KEY");
const launch = player.launchCourse({
el: "#course",
fetch_url: "https://your-storage.example/download/guide",
canonical_url: "https://your-storage.example/training/guide.pdf",
format: "pdf",
user: { name: "John Doe", email: "john@example.com" }
});
launch.onEvent((type, result) => {
console.log(type, result.normalized_details?.seconds);
// getDetails() also returns per-page times in runtime_details.data.media.
});Videos complete after at least 95% unique playback and reaching the end. PDFs require every page to be viewed, plus any optional total/per-page viewing-time rules set in management or the HTTP API. Time counts visible use, excludes hidden tabs/loading, and is not proof of attention. Media reports completion, success: "unknown", and score: null; it does not invent assessment results. normalized_details.media summarizes viewing progress. runtime_details.data.media includes PDF page_seconds and video watched_ranges.
Use the SCORM export HTTP endpoints, or Download as SCORM in management, to distribute a standalone media package or a connected wrapper in another LMS. A connected wrapper uses the LMS's own learner identity and attempts; it does not require browser SDK keys or learner email entry.
Methods and lifecycle
ScormBoxPlayer.initialize(registration_key) or ScormBoxPlayer.initialize({ launch_token }) returns a player instance. Call player.launchCourse(options) to get a playback handle immediately, then subscribe to events and await launch.ready. The examples call this handle launch.
| Method or property | Result |
|---|---|
launch.state | A Promise for the stored snapshot before playback. Includes normalized and runtime details. Button modes read immediately without importing or creating a session. Rejects if the initial read or preparation fails. |
launch.ready | A Promise for the initial saved result. Rejects if preparation or opening fails. |
launch.open() | Button modes: opens the window or overlay and returns a Promise for that opening. Reopening resumes with a fresh session. |
launch.onEvent(callback) | Subscribes to every event as (type, payload). Returns an unsubscribe function. |
launch.on(type, callback) | Subscribes to one event type as (payload). Returns an unsubscribe function. |
launch.getResult() | Returns the latest cached saved result, or null before it is available. |
launch.getDetails() | Returns a Promise for a fresh saved result with format-specific runtime data. |
launch.close() | Returns a Promise for the final saved result after closure is acknowledged. Keeps the player open if saving fails. |
launch.destroy() | Gracefully closes the course and removes its iframe and listeners. Returns a Promise. |
player.destroy() | Gracefully closes all courses owned by this player instance and removes their players. Returns a Promise. |
const unsubscribe = launch.onEvent((type, payload) => console.log(type, payload));
unsubscribe();
const snapshot = launch.getResult(); // Latest cached saved result, or null.
const details = await launch.getDetails(); // Fresh saved result with runtime data.
const finalResult = await launch.close(); // Wait for save acknowledgement; retain frame on failure.
await launch.destroy(); // Gracefully close, then remove iframe/listeners.
await player.destroy(); // Destroy all launches owned by this player instance.Use the package's own Finish/Exit control when it requires one. AICC close cannot force a remote provider to send a HACP report. Abrupt navigation can lose unreported changes. destroy({force: true}) removes the frame immediately without waiting for a final save; its server session expires normally or is replaced the next time the course opens.
Integration boundaries
Your registration key is a public browser identifier, restricted to the project’s website access rules. Signed launch tokens instead restrict the course, learner, attempt generation and origin chosen by your server. Keep server API keys/secrets on your server in both modes. Email sign-in links and library sessions grant private management access. Do not put management links in your LMS's learner pages. Rotate keys or revoke projects in the SDK workspace; this expires affected SDK sessions without deleting imported courses or saved attempts.
Learner identity comes from your integration. Browser results report what the package submitted; they do not independently prove learner identity or certify assessment integrity. Authenticate learners in your own application.
Supported packages can be played through the SDK. Malformed archives, incompatible vendor-specific behavior, content licensing, external authentication, framing restrictions and missing network resources can still prevent playback.
Limits: 500 MB archive, 2 GB expanded, 500 MB per file and 5,000 archive entries. Remote imports require HTTPS, publicly resolved addresses, up to five redirects and a bounded download time. Projects allow up to 20 exact base websites and 500 imported courses; superadmins can add subdomain or any-website access. First-time extraction of large or many-file courses can take longer; subsequent openings reuse the import.
The embedding site's CSP must permit https://scormbox.com in script-src, connect-src and frame-src. If its policy restricts nested frames, permit https://content.scormbox.com too. Third-party cookie restrictions may affect external providers; the SDK's own learner session uses its launch capability rather than third-party account cookies.
Use the plain JavaScript integration example. No package manager or framework is required.