So you need to implement SCORM. Maybe you're building custom eLearning content, integrating a course player, or debugging why a course isn't tracking properly in your LMS. Whatever brought you here, I'm going to walk you through exactly how SCORM works at the technical level.

Not a developer? If you're looking for a high-level overview without the code, check out our What is SCORM? An Executive Guide for L&D Leaders.

Quick note: In practice, most people don't hand-code SCORM packages. Authoring tools like Articulate Storyline, Adobe Captivate, or iSpring handle the packaging for you. But understanding what's inside helps when things go wrong. And things will go wrong.


The Three Core Components of SCORM

SCORM isn't one thing. It's a bundle of related specifications. Understanding these three components is key to working with SCORM effectively:

  1. Content Packaging — how you bundle your files into a ZIP that an LMS can understand
  2. Run-Time Environment — the JavaScript API your content uses to talk to the LMS
  3. Data Model — the specific fields you can read and write (scores, completion, bookmarks, etc.)

1. Content Packaging

Content packaging defines how you bundle your training materials so an LMS can understand them. It's essentially a ZIP file with a specific structure.

At the heart of every SCORM package is imsmanifest.xml. This file tells the LMS everything it needs to know: what files are included, how they're organized, and what should be launched. If you already have a package, you can inspect that file with Edaxu's free SCORM manifest parser.

Here's what a typical SCORM package looks like when unzipped:

scorm-package/
├── imsmanifest.xml          # Required - the manifest file
├── index.html               # Your course entry point
├── js/
│   ├── scorm-api.js        # JavaScript to communicate with the LMS
│   └── course-logic.js     # Your course code
├── css/
│   └── styles.css
├── images/
│   └── ...
└── adlcp_rootv1p2.xsd      # Schema files (usually included)

The manifest file is critical. Here's a working example:

<?xml version="1.0" encoding="UTF-8"?>
<manifest identifier="course_001" version="1.0"
          xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
          xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_rootv1p2">

  <metadata>
    <schema>ADL SCORM</schema>
    <schemaversion>1.2</schemaversion>
  </metadata>

  <organizations default="org_001">
    <organization identifier="org_001">
      <title>Introduction to Safety Training</title>
      <item identifier="item_001" identifierref="resource_001">
        <title>Module 1: Workplace Safety Basics</title>
      </item>
    </organization>
  </organizations>

  <resources>
    <resource identifier="resource_001" type="webcontent"
              adlcp:scormtype="sco" href="index.html">
      <file href="index.html"/>
      <file href="js/scorm-api.js"/>
      <file href="js/course-logic.js"/>
      <file href="css/styles.css"/>
    </resource>
  </resources>
</manifest>

A few things:

  • The identifier attributes must be unique within the manifest
  • adlcp:scormtype="sco" marks this as a Sharable Content Object (something that communicates with the LMS)
  • Every file in your package should be listed under <file> elements
  • File paths are case-sensitive on many systems

2. Run-Time Environment (The API)

This is where the actual communication happens. When a learner launches a SCORM course, the LMS provides a JavaScript API that your content uses to send and receive data.

The API Hunt

Your content runs in a browser window (usually an iframe), but the API object lives in the parent window hierarchy. SCORM defines a standard way to find it:

function findAPI(win) {
  let attempts = 0;
  const maxAttempts = 500;

  // Search up through parent windows
  while (win.API == null && win.parent != null &&
         win.parent != win && attempts < maxAttempts) {
    attempts++;
    win = win.parent;
  }

  // If not found, check the opener window
  if (win.API == null && win.opener != null) {
    win = win.opener;
    while (win.API == null && win.parent != null &&
           win.parent != win && attempts < maxAttempts) {
      attempts++;
      win = win.parent;
    }
  }

  return win.API;
}

For SCORM 2004, look for API_1484_11 instead of API. The weird name comes from the IEEE standard number.

The Eight Core API Methods

Once you've found the API, you have eight methods to work with:

SCORM 1.2SCORM 2004Purpose
LMSInitialize("")Initialize("")Start the session
LMSFinish("")Terminate("")End the session
LMSGetValue(element)GetValue(element)Read data
LMSSetValue(element, value)SetValue(element, value)Write data
LMSCommit("")Commit("")Persist data
LMSGetLastError()GetLastError()Get error code
LMSGetErrorString(code)GetErrorString(code)Get error message
LMSGetDiagnostic(code)GetDiagnostic(code)Get error details

A Basic SCORM Session

Here's what a complete SCORM interaction looks like:

// Find the API
const API = findAPI(window);

if (!API) {
  console.error('SCORM API not found - running outside LMS?');
  // Decide whether to continue or abort
}

// Initialize the connection
const initResult = API.LMSInitialize('');
if (initResult !== 'true') {
  const errorCode = API.LMSGetLastError();
  const errorMsg = API.LMSGetErrorString(errorCode);
  console.error(`SCORM init failed: ${errorMsg}`);
}

// Read learner info
const learnerName = API.LMSGetValue('cmi.core.student_name');
const learnerId = API.LMSGetValue('cmi.core.student_id');
console.log(`Welcome, ${learnerName} (${learnerId})`);

// Check existing progress
const lessonStatus = API.LMSGetValue('cmi.core.lesson_status');
const bookmark = API.LMSGetValue('cmi.core.lesson_location');

if (lessonStatus === 'completed' || lessonStatus === 'passed') {
  // Maybe show a "review mode" message
}

if (bookmark) {
  // Resume from where they left off
  jumpToLocation(bookmark);
}

// When the learner progresses...
function saveProgress(location) {
  API.LMSSetValue('cmi.core.lesson_location', location);
  API.LMSCommit(''); // Important: persist the data
}

// When they complete the course...
function completeCourse(score) {
  API.LMSSetValue('cmi.core.lesson_status', 'completed');
  API.LMSSetValue('cmi.core.score.raw', score.toString());
  API.LMSCommit('');
}

// When they leave - CRITICAL: always call this
function closeCourse() {
  API.LMSFinish('');
}

// Make sure LMSFinish gets called no matter how they exit
window.addEventListener('beforeunload', closeCourse);
window.addEventListener('unload', closeCourse);

Critical: Always call LMSFinish (or Terminate in SCORM 2004). If you don't, the LMS might not save the session data. I can't tell you how many "my completion didn't track" bugs come down to a missing LMSFinish call when the browser closes unexpectedly.

3. The Data Model

SCORM defines specific elements you can read and write. You can't just make up your own field names—you have to use the standard data model elements.

Essential Data Model Elements (SCORM 1.2)

// Learner Identity
cmi.core.student_id       // Read-only - learner's ID
cmi.core.student_name     // Read-only - learner's name

// Status and Completion
cmi.core.lesson_status    // "not attempted", "incomplete", "completed",
                          // "passed", "failed"
cmi.core.entry            // "ab-initio" (new), "resume" (returning), or ""

// Score
cmi.core.score.raw        // Numeric score (e.g., "85")
cmi.core.score.min        // Minimum possible score
cmi.core.score.max        // Maximum possible score

// Time Tracking
cmi.core.session_time     // How long this session lasted (format: "HH:MM:SS")
cmi.core.total_time       // Read-only - cumulative time across sessions

// Bookmarking
cmi.core.lesson_location  // Free-form string to store position (max 255 chars)

// Custom Data
cmi.suspend_data          // Free-form string for custom data (max 4096 chars)
cmi.launch_data           // Read-only - data passed from manifest

SCORM 2004 Differences

The data model paths are slightly different in SCORM 2004:

// SCORM 1.2              // SCORM 2004
cmi.core.student_id       → cmi.learner_id
cmi.core.student_name     → cmi.learner_name
cmi.core.lesson_status    → cmi.completion_status + cmi.success_status
cmi.core.score.raw        → cmi.score.raw + cmi.score.scaled
cmi.core.session_time     → cmi.session_time (ISO 8601 duration)
cmi.core.lesson_location  → cmi.location
cmi.suspend_data          → cmi.suspend_data (64KB limit vs 4KB)

The big change in SCORM 2004 is splitting lesson_status into two separate fields: completion_status (completed/incomplete) and success_status (passed/failed). This gives you more granular control.

Tracking Quiz Interactions

SCORM lets you record individual question responses, which is invaluable for analytics:

// Record a multiple-choice question
API.LMSSetValue('cmi.interactions.0.id', 'question_safety_001');
API.LMSSetValue('cmi.interactions.0.type', 'choice');
API.LMSSetValue('cmi.interactions.0.student_response', 'A');
API.LMSSetValue('cmi.interactions.0.correct_responses.0.pattern', 'B');
API.LMSSetValue('cmi.interactions.0.result', 'incorrect');
API.LMSSetValue('cmi.interactions.0.weighting', '1');
API.LMSSetValue('cmi.interactions.0.latency', '00:00:45'); // Time to answer

// Record a true/false question
API.LMSSetValue('cmi.interactions.1.id', 'question_safety_002');
API.LMSSetValue('cmi.interactions.1.type', 'true-false');
API.LMSSetValue('cmi.interactions.1.student_response', 't');
API.LMSSetValue('cmi.interactions.1.correct_responses.0.pattern', 't');
API.LMSSetValue('cmi.interactions.1.result', 'correct');

// Record a fill-in-the-blank
API.LMSSetValue('cmi.interactions.2.id', 'question_safety_003');
API.LMSSetValue('cmi.interactions.2.type', 'fill-in');
API.LMSSetValue('cmi.interactions.2.student_response', 'fire extinguisher');
API.LMSSetValue('cmi.interactions.2.correct_responses.0.pattern', 'fire extinguisher');
API.LMSSetValue('cmi.interactions.2.result', 'correct');

API.LMSCommit('');

The interaction index (0, 1, 2...) increments for each new question. Interaction types include: choice, true-false, fill-in, matching, performance, sequencing, likert, numeric.


SCORM 1.2 vs SCORM 2004: Technical Differences

The API naming isn't the only thing that changed. Here's what actually matters:

Sequencing and Navigation

SCORM 1.2 has no sequencing specification. If you want to control navigation (e.g., "complete Module 1 before accessing Module 2"), you build that logic in your course JavaScript.

SCORM 2004 added a complex sequencing specification that lets you define rules in the manifest. The LMS then enforces navigation. It's powerful but notoriously difficult to implement correctly.

<!-- SCORM 2004 Sequencing Example -->
<sequencing>
  <controlMode choice="false" flow="true"/>
  <sequencingRules>
    <preConditionRule>
      <ruleConditions conditionCombination="all">
        <ruleCondition condition="completed" referencedObjective="obj_module1"/>
      </ruleConditions>
      <ruleAction action="disabled"/>
    </preConditionRule>
  </sequencingRules>
</sequencing>

Most developers I know avoid SCORM 2004 sequencing unless specifically required. It's hundreds of pages of specification, and debugging issues is painful.

Data Limits

Data ElementSCORM 1.2SCORM 2004
suspend_data4,096 chars64,000 chars
lesson_location/location255 chars1,000 chars
comments_from_learner4,096 chars64,000 chars

If you're building a complex simulation or branching scenario that needs to save a lot of state, SCORM 1.2's 4KB limit can be a real constraint.

Time Format

// SCORM 1.2 - HH:MM:SS.ss format
API.LMSSetValue('cmi.core.session_time', '00:45:30.50');

// SCORM 2004 - ISO 8601 duration format
API.SetValue('cmi.session_time', 'PT45M30.5S'); // 45 minutes, 30.5 seconds

Error Handling

SCORM 2004 has more detailed error codes and better diagnostic information. But in practice, error handling in SCORM is minimal—most issues surface as tracking failures that you debug through logging.


Building a SCORM Wrapper

Rather than calling API methods directly throughout your code, wrap them in a clean interface:

const SCORM = (function() {
  let API = null;
  let initialized = false;

  function findAPI(win) {
    let attempts = 0;
    while (win.API == null && win.parent != null &&
           win.parent != win && attempts < 500) {
      attempts++;
      win = win.parent;
    }
    if (win.API == null && win.opener != null) {
      return findAPI(win.opener);
    }
    return win.API;
  }

  function init() {
    API = findAPI(window);
    if (!API) {
      console.warn('SCORM API not found');
      return false;
    }

    const result = API.LMSInitialize('');
    initialized = (result === 'true');

    if (!initialized) {
      console.error('SCORM initialization failed:', getError());
    }

    return initialized;
  }

  function terminate() {
    if (!initialized) return true;

    const result = API.LMSFinish('');
    initialized = false;
    return result === 'true';
  }

  function get(element) {
    if (!initialized) return '';
    return API.LMSGetValue(element);
  }

  function set(element, value) {
    if (!initialized) return false;
    return API.LMSSetValue(element, value) === 'true';
  }

  function save() {
    if (!initialized) return false;
    return API.LMSCommit('') === 'true';
  }

  function getError() {
    if (!API) return 'API not found';
    const code = API.LMSGetLastError();
    return API.LMSGetErrorString(code);
  }

  // Public interface
  return {
    init,
    terminate,
    get,
    set,
    save,
    getError,

    // Convenience methods
    getStudentName: () => get('cmi.core.student_name'),
    getStudentId: () => get('cmi.core.student_id'),

    setStatus: (status) => set('cmi.core.lesson_status', status),
    setScore: (score) => set('cmi.core.score.raw', score.toString()),
    setLocation: (loc) => set('cmi.core.lesson_location', loc),
    getLocation: () => get('cmi.core.lesson_location'),

    setSuspendData: (data) => set('cmi.suspend_data',
                                  typeof data === 'string' ? data : JSON.stringify(data)),
    getSuspendData: () => {
      const data = get('cmi.suspend_data');
      try { return JSON.parse(data); }
      catch { return data; }
    }
  };
})();

// Usage
window.addEventListener('load', () => {
  if (SCORM.init()) {
    console.log('Hello,', SCORM.getStudentName());

    const savedState = SCORM.getSuspendData();
    if (savedState) {
      restoreState(savedState);
    }
  }
});

window.addEventListener('beforeunload', () => {
  SCORM.terminate();
});

There are also open-source wrappers available, like pipwerks SCORM API Wrapper, that handle edge cases and cross-version compatibility.


When Things Break

"The LMS Won't Import My SCORM Package"

Symptoms: Upload fails or course appears but won't launch.

Likely causes:

  1. imsmanifest.xml isn't at the ZIP root (most common)
  2. Invalid XML syntax in the manifest
  3. File paths don't match actual files (case-sensitivity!)
  4. Missing required manifest elements
  5. Corrupted ZIP file

Debug steps:

# Unzip and check structure
unzip -l course.zip | head -20
# manifest should be at the root level, not in a subfolder

# Validate XML
xmllint --noout imsmanifest.xml

# Check for case mismatches
find . -name "*.html" -o -name "*.HTML"

"Course Launches But Doesn't Track"

Symptoms: Course opens, learner completes it, but LMS shows no progress.

Likely causes:

  1. LMSInitialize never called or failed
  2. LMSSetValue called but LMSCommit missing
  3. LMSFinish not called on exit
  4. Wrong data model element names
  5. LMS expects specific status values

Debug with console logging:

// Add this to your SCORM wrapper
function logAPICall(method, args, result) {
  console.log(`SCORM: ${method}(${args.join(', ')}) → ${result}`);
}

// Then wrap your API calls
const origInit = API.LMSInitialize;
API.LMSInitialize = function(arg) {
  const result = origInit.call(API, arg);
  logAPICall('LMSInitialize', [arg], result);
  return result;
};
// ... same for other methods

"Works in LMS A But Not LMS B"

Symptoms: Identical package works in one LMS, fails in another.

Likely causes:

  1. Different timing requirements (some LMSs need delays between calls)
  2. Different iframe/window configurations
  3. Different strictness in validating values
  4. Security policies blocking cross-origin access

Solutions:

  • Add small delays between API calls: setTimeout(() => API.LMSCommit(''), 100)
  • Test in Edaxu first by uploading your package and confirming the tracked completion, score, and bookmark data
  • Check browser console for CORS or security errors
  • Verify your API hunt checks both parent and opener windows

"Suspend Data Not Persisting"

Symptoms: Custom data saves during session but is gone when learner returns.

Likely causes:

  1. Exceeding the size limit (4KB for SCORM 1.2)
  2. Missing LMSCommit after LMSSetValue
  3. LMSFinish not being called
  4. Special characters causing issues

Fix:

// Compress data and check size
function saveSuspendData(data) {
  const json = JSON.stringify(data);

  // SCORM 1.2 limit
  if (json.length > 4096) {
    console.error(`Suspend data too large: ${json.length} chars (max 4096)`);
    return false;
  }

  const result = API.LMSSetValue('cmi.suspend_data', json);
  API.LMSCommit('');
  return result === 'true';
}

Debugging Checklist

  1. Open browser DevTools console - Watch for JavaScript errors
  2. Log all API calls - See exactly what's being sent to the LMS
  3. Test in Edaxu - Upload your package and confirm completion, score, and bookmark behavior
  4. Validate manifest - Use an XML validator or SCORM validation tool
  5. Check network tab - Look for failed requests or CORS issues
  6. Test the close button - Make sure LMSFinish fires on all exit paths

Testing Your SCORM Package

Before uploading to production:

1. Validate the Structure

# Create the package correctly
cd your-course-folder
zip -r ../course.zip *  # NOT: zip -r course.zip your-course-folder/

# Verify manifest is at root
unzip -l course.zip | grep imsmanifest
# Should show: imsmanifest.xml (not folder/imsmanifest.xml)

2. Validate the Manifest

Validate your imsmanifest.xml before uploading anywhere. At minimum, confirm the XML is well formed, the manifest sits at the package root, every referenced launch file exists, and the declared SCORM version matches what your LMS expects. Edaxu's free SCORM manifest parser can help you inspect the package structure before you upload it to an LMS.

This catches things like malformed XML, missing required elements, and file path mismatches before they become production problems.

3. Test Your Full Package

Want to test the complete package—not just the manifest? Start a free Edaxu trial and test your first SCORM course to see exactly how it behaves in a real LMS environment. You can check what data is being tracked and catch issues before rolling out to learners.

4. Test in Your Target LMS

Don't assume compatibility. Test the actual package in the actual LMS your learners will use. Check:

  • Course launches correctly
  • Progress saves when closing mid-course
  • Completion registers when finishing
  • Score records accurately
  • Bookmarking works for returning learners

Quick Reference: Common API Patterns

// Minimum viable SCORM (just track completion)
API.LMSInitialize('');
// ... course content ...
API.LMSSetValue('cmi.core.lesson_status', 'completed');
API.LMSCommit('');
API.LMSFinish('');

// Track completion with score
API.LMSSetValue('cmi.core.lesson_status', 'passed');
API.LMSSetValue('cmi.core.score.raw', '85');
API.LMSSetValue('cmi.core.score.min', '0');
API.LMSSetValue('cmi.core.score.max', '100');

// Bookmark current position
API.LMSSetValue('cmi.core.lesson_location', 'slide_15');

// Save complex state
const state = { currentSlide: 15, answers: [1, 2, 1, 3], branch: 'A' };
API.LMSSetValue('cmi.suspend_data', JSON.stringify(state));

// Restore state on return
const savedState = JSON.parse(API.LMSGetValue('cmi.suspend_data') || '{}');

Looking for the business perspective? If you need to explain SCORM to stakeholders or understand its role in L&D strategy, see our executive guide to SCORM for L&D leaders.


Ready to See SCORM in Action?

Skip the compatibility headaches. Edaxu supports both SCORM 1.2 and SCORM 2004 out of the box—just upload your packages and start testing. Start a free trial, see exactly what data your courses are sending, and debug tracking issues before they become problems.


Frequently Asked Questions

Should I use SCORM 1.2 or SCORM 2004 for my project?

Honestly? SCORM 1.2, unless you have a specific reason not to.

SCORM 2004's extra features—larger suspend_data, LMS-controlled sequencing, split completion/success status—sound nice on paper. But most projects never use them, and you'll spend extra time dealing with compatibility edge cases. Start with 1.2. You can always upgrade later if you hit its limits.

Why is my LMSFinish call not working?

This one bites everyone eventually. Usually it's one of these:

  • Your API handle went stale (you stored it in a variable that's now undefined)
  • The iframe got unloaded before your unload handler ran
  • A JavaScript error earlier in your cleanup code killed execution

My advice: call LMSFinish as the absolute first thing in your unload handler, before any other cleanup logic. And test by actually closing the browser tab, not just clicking a "close" button.

How do I handle courses that run outside an LMS?

Check for the API first and gracefully degrade:

const API = findAPI(window);
if (API) {
  // Full SCORM tracking
} else {
  // Standalone mode - maybe use localStorage for progress
  console.log('Running in standalone mode');
}

What's the best way to debug SCORM tracking issues?

Log every API call with its parameters and return value. Use browser DevTools to watch the console. Upload to Edaxu to test in a real LMS environment and rule out package issues. Check that LMSCommit is called after LMSSetValue and that LMSFinish fires on all exit paths.

Can I track custom data beyond what SCORM provides?

Yep—that's what cmi.suspend_data is for. It's a free-form string field, so most developers just JSON.stringify their state object and store it there.

The catch is size limits: 4KB for SCORM 1.2, 64KB for SCORM 2004. If you're building something complex (branching simulations, detailed interaction logs), you might hit that wall. At that point, either compress your data, be more selective about what you store, or look into xAPI which doesn't have these constraints.

How do I convert between SCORM 1.2 and SCORM 2004?

It's not just a find-and-replace job, unfortunately. You'll need to:

  • Update the manifest XML schema references
  • Change all your API method names (LMSInitializeInitialize, etc.)
  • Update data model paths (cmi.core.student_namecmi.learner_name)
  • Handle the split status fields in 2004 (lesson_status becomes completion_status + success_status)

If you're using an authoring tool like Storyline or Captivate, the easy path is just re-exporting to the target version. Hand-converting custom code is tedious but doable.


Further Reading and Sources

ET
Edaxu Team
matt@edaxu.com

Edaxu writes practical guides for teams running SCORM, certification, compliance, and professional training programs.