📦 Create your xAPI export for an LRS
The xAPI format (Experience API, formerly Tin Can API) allows for much finer tracking than SCORM: instead of just reporting a status and a score to an LMS, your export sends "statements" (activity records) directly to an LRS (Learning Record Store), with or without an LMS around it.
Defining the xAPI format
An xAPI statement reads like an actor - verb - object sentence, sent via HTTP request to the LRS:
- Actor: who performed the action (learner's email or identifier)
- Verb: the action performed (e.g. initialized, completed, passed, progressed)
- Object: the activity involved (your Celestory module)
- Result (optional): score, success, progress, duration...
Characteristics of the xAPI format
- Can work without an LMS: your export can send its statements directly to a standalone LRS (Learning Locker, Watershed, SCORM Cloud LRS...)
- Granular tracking: every interaction can be reported, not just a final score like in SCORM
- Defines no automatic completion rule: you decide when each statement is sent
- ⚠️ Unlike SCORM (which communicates with the parent window), xAPI sends real network requests: the LRS must allow CORS from your export's origin, or you'll get silent failures (check the console)
Example resource
Block placement in the graph follows the exact same logic as the SCORM tutorial: reuse the SCORM example graph (https://creator.celestory.io/project/phHUew7e6) and replace the SCORM blocks with the xAPI blocks below, at the same spots (start, intermediate steps, end).
Important note: Only the blocks starting with xAPI need to be added wherever you like in your graph.
Step 1: Add the Javascript blocks
Block 1: xAPI - Configuration
window.XAPI = {
endpoint: "https://YOUR-LRS.example.com/xapi/",
auth: "Basic YOUR_KEY_IN_BASE64",
actor: {
objectType: "Agent",
name: "Celestory Player",
mbox: "mailto:player@example.com"
},
activityId: "https://yourdomain.com/activities/module-name"
};
Get the endpoint and authentication key from your LRS (or your LMS, if the LRS is built into it). If your export is launched from a Tin Can Launch-compatible LMS, you can replace these fixed values with a read of the parameters passed in the URL at launch instead of hardcoding them.
Block 2: xAPI - Send function + Initialize
function xAPI_sendStatement(verbId, verbDisplay, resultObj) {
const statement = {
actor: window.XAPI.actor,
verb: {
id: verbId,
display: { "en-US": verbDisplay }
},
object: {
id: window.XAPI.activityId,
objectType: "Activity"
}
};
if (resultObj) {
statement.result = resultObj;
}
fetch(window.XAPI.endpoint + "statements", {
method: "POST",
headers: {
"Authorization": window.XAPI.auth,
"Content-Type": "application/json",
"X-Experience-API-Version": "1.0.3"
},
body: JSON.stringify(statement)
}).then(() => console.log("xAPI: statement sent -", verbDisplay))
.catch(error => console.log("xAPI send error", error));
}
xAPI_sendStatement("http://adlnet.gov/expapi/verbs/initialized", "initialized");
The verbId (in English, standardized by the ADL) is what matters for interoperability. The verbDisplay is just a human-readable label shown in the LRS, feel free to translate it.
Block 3: xAPI - Step
const step = 70;
xAPI_sendStatement(
"http://adlnet.gov/expapi/verbs/progressed",
"progressed",
{
extensions: {
"http://id.tincanapi.com/extension/progress": step
}
}
);
You can add as many steps as you like by duplicating this block with different percentages (e.g. 10%, 50%, 70%), just like the SCORM steps.
Block 4: xAPI - Complete
const scorePercent = 100;
xAPI_sendStatement(
"http://adlnet.gov/expapi/verbs/completed",
"completed",
{
score: {
scaled: scorePercent / 100,
raw: scorePercent,
min: 0,
max: 100
},
completion: true,
success: true
}
);
Block 5: xAPI - Finish Session
xAPI_sendStatement("http://adlnet.gov/expapi/verbs/terminated", "terminated");
console.log("xAPI: session ended");
Step 2: Export the project
Export your project as Web or PWA.
Step 3: (optional) Add a tincan.xml file
Some LMS/LRS require a Tin Can package rather than a simple direct link. If so, add a tincan.xml file at the root of your export:
<?xml version="1.0" encoding="utf-8"?>
<tincan xmlns="http://projecttincan.com/tincan.xsd">
<activities>
<activity id="https://yourdomain.com/activities/module-name" type="http://adlnet.gov/expapi/activities/module">
<name>Module name</name>
<description lang="en-US">Description of your Celestory module</description>
<launch lang="en-US">index.html</launch>
</activity>
</activities>
</tincan>
Step 4: Compress and test
If a package is required, compress your folder into a ZIP (as with SCORM). If your LRS receives statements directly over HTTP, you can deploy the Web content as-is (e.g. Direct Link or HTML5 export).
To test, you can use a site like https://app.cloud.scorm.com/sc/user/Home (Tin Can/xAPI compatible) or check the statement stream received directly in your LRS's dashboard.
Updated on: 02/07/2026
Thank you!
