> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://documentation.celestory.io/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# 📦 Create your cmi5 export for an LMS

cmi5 is a standardized xAPI profile designed to replace SCORM in modern LMSs: it keeps the launch flow managed by an LMS (like SCORM), but routes xAPI statements to an LRS for much finer tracking.
### **Defining the cmi5 format**
Unlike SCORM (a simple global API) and "free-form" xAPI (statements sent whenever you like), cmi5 enforces a strict sequence of statements, sent in this order:
* **Initialized**: when the module launches
* **Completed**: once the content has been gone through
* **Passed** / **Failed**: depending on the score obtained
* **Terminated**: on close, always last
### **Characteristics of the cmi5 format**
* Requires a cmi5-compatible LMS and LRS: the LMS provides an authentication token to fetch dynamically at launch (unlike "free-form" xAPI, where credentials can be hardcoded)
* Tracking as precise as classic xAPI (detailed score, progress, pass/fail)
* Course structure defined by a **cmi5.xml** file (the equivalent of SCORM's imsmanifest.xml)
* ⚠️ Cannot be tested outside a cmi5-compatible LMS/LRS: session identifiers are provided dynamically at launch, so they can't be hardcoded like with standalone xAPI
* ⚠️ As with xAPI, statements are sent over direct HTTP: the LRS must allow CORS from your export's origin
### **Example resource**
Block placement in the graph follows the same logic as the [__SCORM tutorial__](https://documentation.celestory.io/fr/article/creer-son-export-scorm-pour-lms-d3ne2m/): reuse the SCORM example graph ([https://creator.celestory.io/project/phHUew7e6](https://creator.celestory.io/project/phHUew7e6)) and replace the SCORM blocks with the cmi5 blocks below, at the same spots (start, intermediate steps, end).
**Important note:** Only the blocks starting with cmi5 need to be added wherever you like in your graph.
### **Step 1: Add the Javascript blocks**
#### Block 1: cmi5 - Retrieve launch parameters
```
function cmi5_getParam(name) {
    const url = new URL(window.location.href);
    return url.searchParams.get(name);
}
​
window.CMI5 = {
    endpoint: cmi5_getParam("endpoint"),
    fetchUrl: cmi5_getParam("fetch"),
    actor: JSON.parse(cmi5_getParam("actor")),
    registration: cmi5_getParam("registration"),
    activityId: cmi5_getParam("activityId"),
    authToken: null
};
​
if (window.CMI5.endpoint && !window.CMI5.endpoint.endsWith("/")) {
    window.CMI5.endpoint += "/";
}
```
These parameters (endpoint, fetch, actor, registration, activityId) are automatically appended to your module's URL by the LMS at launch time: nothing to hardcode here.
#### Block 2: cmi5 - Send function + token fetch + Initialize
```
function cmi5_sendStatement(verbId, verbDisplay, resultObj) {
    const category = [{ id: "https://w3id.org/xapi/cmi5/context/categories/cmi5" }];
​
    const moveOnVerbs = [
        "http://adlnet.gov/expapi/verbs/completed",
        "http://adlnet.gov/expapi/verbs/passed",
        "http://adlnet.gov/expapi/verbs/failed"
    ];
    if (moveOnVerbs.includes(verbId)) {
        category.push({ id: "https://w3id.org/xapi/cmi5/context/categories/moveOn" });
    }
​
    const statement = {
        actor: window.CMI5.actor,
        verb: {
            id: verbId,
            display: { "en-US": verbDisplay }
        },
        object: {
            id: window.CMI5.activityId,
            objectType: "Activity"
        },
        context: {
            registration: window.CMI5.registration,
            contextActivities: { category: category }
        }
    };
​
    if (resultObj) {
        statement.result = resultObj;
    }
​
    fetch(window.CMI5.endpoint + "statements", {
        method: "POST",
        headers: {
            "Authorization": window.CMI5.authToken,
            "Content-Type": "application/json",
            "X-Experience-API-Version": "1.0.3"
        },
        body: JSON.stringify(statement)
    }).then(() => console.log("cmi5: statement sent -", verbDisplay))
      .catch(error => console.log("cmi5 send error", error));
}
​
fetch(window.CMI5.fetchUrl, { method: "POST" })
    .then(response => response.json())
    .then(data => {
        window.CMI5.authToken = "Basic " + data["auth-token"];
        cmi5_sendStatement("http://adlnet.gov/expapi/verbs/initialized", "initialized");
    })
    .catch(error => console.log("cmi5 token fetch error", error));
```
The token retrieved via `fetch` is valid for the whole session: it's what authenticates every statement that follows.
#### Block 3: cmi5 - Step
```
const step = 70;
​
cmi5_sendStatement(
    "http://adlnet.gov/expapi/verbs/progressed",
    "progressed",
    {
        extensions: {
            "https://w3id.org/xapi/cmi5/result/extensions/progress": step
        }
    }
);
```
You can add as many steps as you like by duplicating this block with different percentages, just like the SCORM steps.
#### Block 4: cmi5 - Completed + Passed
```
const scorePercent = 100;
​
cmi5_sendStatement("http://adlnet.gov/expapi/verbs/completed", "completed", {
    completion: true,
    duration: "PT0H5M0S"
});
​
cmi5_sendStatement("http://adlnet.gov/expapi/verbs/passed", "passed", {
    score: {
        scaled: scorePercent / 100,
        raw: scorePercent,
        min: 0,
        max: 100
    },
    success: true
});
```
`duration` follows the ISO 8601 format (e.g. PT0H5M0S = 5 minutes) — adjust or compute it dynamically based on the actual time spent. On failure, send the **failed** verb (`http://adlnet.gov/expapi/verbs/failed`) with `success: false` instead of passed.
#### Block 5: cmi5 - Finish Session (always last)
```
cmi5_sendStatement("http://adlnet.gov/expapi/verbs/terminated", "terminated");
console.log("cmi5: session ended");
```
The Terminated statement must always be the last one sent: no other statement should go out after this one.
### **Step 2: Export the project**
Export your project as Web or PWA.
### **Step 3: Create the folder structure**
Create a folder titled with your module's name (e.g. module1) and move all the exported content into it.
### **Step 4: Add the cmi5.xml file**
At the root of your package (next to the module1 folder), add a **cmi5.xml** file describing the course structure:
```
<?xml version="1.0" encoding="UTF-8"?>
<courseStructure xmlns="https://w3id.org/xapi/profiles/cmi5/v1/CourseStructure.xsd">
    <course id="https://yourdomain.com/courses/course-name">
        <title><langstring lang="en-US">Course name</langstring></title>
        <description><langstring lang="en-US">Course description</langstring></description>
    </course>
    <au id="https://yourdomain.com/courses/course-name/module1" moveOn="CompletedOrPassed">
        <title><langstring lang="en-US">Module name</langstring></title>
        <description><langstring lang="en-US">Module description</langstring></description>
        <url>module1/index.html</url>
    </au>
</courseStructure>
```
The `<au>`'s `id` must match the `activityId` the LMS will pass at launch: that's what links the received statements back to the right module. `moveOn` sets the completion condition (`Completed`, `Passed`, `CompletedAndPassed`, `CompletedOrPassed`, or `NotApplicable`).
### **Step 5: Compress into a ZIP**
Compress the parent folder (containing cmi5.xml and the module1 folder) into a zip file.
### **Step 6: Test the cmi5 file**
Import the zip into an LMS or a cmi5-compatible tester, for example [https://app.cloud.scorm.com/sc/user/Home](https://app.cloud.scorm.com/sc/user/Home) (SCORM Cloud also handles cmi5). Launch the module then check in the LRS that the statements arrive in order: Initialized, Completed, Passed (or Failed), Terminated.