We have a course hosted on our server. We are trying to make it SCORM compatible using the pipwerks API. Initially when we uploaded the course to SCORM Cloud, it led to a cross-domain error. So we tried out the postMessage HTML solution suggested here: Easiest way to convert SCORM package to run on an external server?
Thereafter, we were able to connect and fetch learner details from SCORM Cloud. But unfortunately, we are still unable to update the status and score in SCORM Cloud. At the moment, as soon as we open the course, SCORM Cloud automatically sets the completion and passed status within a few seconds.
This is our first time working with SCORM, so I am not sure if the configuration of the index.html file is right. I am sharing it below.
The console shows the following errors: Console errors
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LMS</title>
<!-- Load pipwerks SCORM wrapper (assuming it's hosted) -->
<script src="script.js" defer></script>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
#scorm-iframe {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<iframe id="scorm-iframe" frameborder="0"></iframe>
<script>
let Scormdata = {
lenname: '',
lenEmail: '',
params: 'abc',
learnerId: 0,
courseId: 0,
currentUrl: '',
};
const baseUrl = "https://sample.co";
let dataGet = "";
const allowedOrigins = [
"https://sample.co",
"https://sample.co"
];
// ✅ Message Listener
window.addEventListener("message", function (event) {
if (!allowedOrigins.includes(event.origin)) return;
console.log("📩 Message received:", event.data);
if (event.data === "FIND_SCORM_API") {
console.log("📩 SCORM API request received...");
const scormIframe = document.getElementById("scorm-iframe");
if (!scormIframe || !scormIframe.contentWindow) {
console.error("❌ SCORM iframe not found.");
return;
}
const api = pipwerks.SCORM.API;
// Notify parent that SCORM API is found
if (event.source && typeof event.source.postMessage === "function") {
event.source.postMessage(
{ type: "SCORM_API_FOUND", apiAvailable: !!api },
event.origin
);
console.log("✅ Sent SCORM API response to parent.", api);
} else {
console.warn("⚠️ Cannot send SCORM API response; event.source missing.");
}
}
// SCORM init response
if (event.data && event.data.type === "scorm-init-response") {
console.log("✅ SCORM Init Response:", event.data.success ? "Success" : "Failed");
}
// SCORM API response
if (event.data.type === "SCORM_API_RESPONSE") {
console.log("✅ SCORM API is available:", event.data.apiAvailable);
}
// Handle SCORM Score Update
if (event.data.type === "SCORM_SCORE_UPDATE") {
try {
const score = event.data.score;
console.log("✅ Score received:", score);
pipwerks.SCORM.init();
pipwerks.SCORM.setValue("cmi.score.raw", score);
pipwerks.SCORM.commit();
pipwerks.SCORM.finish();
console.log("✅ Score updated in SCORM Cloud:", score);
} catch (error) {
console.error("❌ Error parsing SCORM score data:", error);
}
}
});
// ✅ Initialize SCORM and send init message to iframe
function initializeSCORM() {
const iframe = document.getElementById("scorm-iframe");
iframe.onload = () => {
console.log("✅ SCORM iframe loaded. Sending SCORM init request...");
iframe.contentWindow.postMessage({ type: "scorm-init" }, "*");
};
}
// ✅ Load SCORM learner data and set iframe source
function loadScormPackage() {
if (pipwerks.SCORM.init()) {
const learnerId = pipwerks.SCORM.getValue("cmi.learner_id");
const learnerName = pipwerks.SCORM.getValue("cmi.learner_name");
const learnerEmail = pipwerks.SCORM.getValue("cmi.learner_email"); // Optional
const completionStatus = pipwerks.SCORM.getValue("cmi.completion_status");
const score = pipwerks.SCORM.getValue("cmi.score.raw");
const courseId = pipwerks.SCORM.getValue("cmi.entry");
console.log("Learner ID:", learnerId);
console.log("Learner Name:", learnerName);
console.log("Email:", learnerEmail);
console.log("Completion Status:", completionStatus);
console.log("Score:", score);
console.log("Course ID:", courseId);
const currentUrl = window.location.href;
if (learnerId && learnerName) {
Scormdata = {
...Scormdata,
learnerId,
lenname: learnerName,
lenEmail: learnerEmail,
courseId,
currentUrl
};
dataGet = encodeURIComponent(JSON.stringify(Scormdata));
const fullUrl = baseUrl + dataGet;
console.log("🌐 Iframe URL:", fullUrl);
document.getElementById("scorm-iframe").src = fullUrl;
}
} else {
console.error("❌ SCORM API initialization failed.");
}
}
// ✅ On load: initialize SCORM and load data
window.onload = () => {
initializeSCORM();
loadScormPackage();
};
</script>
</body>
</html>
No offense, but your code is quite the hodgepodge; it's mixing up standard SCORM syntax with the pipwerks wrapper syntax and trying to invoke a bunch of functions/methods that don't exist.
To your direct question, the sequence in which you make your calls is vital. The SCORM API is fragile and is not a RESTful API. It doesn't work the way you'd use a modern API in a system like Postman. You connect to it once when you initialize a course session, and you must keep the connection active via JS in your parent window. If you break the API connection, intentionally or otherwise, it will automatically terminate the course session. The code you provided tries to initialize and terminate a SCORM session every time you receive a SCORM_SCORE_UPDATE
message. This will fail.
Your code should create a reference to the API globally (via the SCORM wrapper) then use that reference for the duration of the session. For example:
//In the course's parent window
const allowedOrigins = [
"https://sample.co",
"https://sample.co"
];
let myVars = {
learner_name: ''
};
const scorm = pipwerks.scorm;
let success = scorm.init();
if(success){
//do stuff like fetch student name
myVars.learner_name = scorm.get("cmi.core.student_name");
} else {
console.log("Course failed to initialize");
return false; //exit before trying any other stuff since there's no point
}
//listen to messages from the child iframe. For this example,
//listening specifically for the message strings "bookmark" or "exit"
window.addEventListener("message", function (event) {
if (!allowedOrigins.includes(event.origin)) return;
const data = JSON.parse(event.data);
switch(Object.keys(data)[0]){
case "bookmark":
scorm.set("cmi.core.lesson_location", data[0]);
scorm.save(); //always save after setting
break;
case "exit":
scorm.quit();
break;
// etc.
}
});
Also:
there is no need to reference pipwerks.SCORM.API const api = pipwerks.SCORM.API;
there is no such field as learner_email
. See the references https://scorm.com/scorm-explained/technical-scorm/run-time/run-time-reference/#section-2
there is no pipwerks.SCORM.commit()
SCORM 1.2 uses API.LMSCommit("")
whereas the pipwerks wrapper uses the more conventional scorm.save()
. Same for finish()
-- this does not exist. SCORM 1.2 uses API.LMSFinish("")
and the wrapper uses scorm.quit()
. I'm not sure where you got those method names.
I highly recommend creating a course that works without all of the cross-domain stuff, so you can be confident it behaves as you wish. Then add the postMessage abstraction to handle cross-domain scripting.
Lastly, be sure to pick either SCORM 1.2 or 2004 then stick with it. The syntax and cmi data fields are not interchangable. They also use different manifests.
Good luck