SDK & Libraries
You can call the edu.games LRS directly over HTTP, or use one of these libraries to simplify xAPI integration in your game.
The first-party SDK. It discovers the launch context the player injects (endpoint, per-session auth, actor, registration) — your game never handles credentials — and gives you typed statement builders that follow the emission profile exactly: batching, flush-on-close, retry, and dedupe ids are built in. Load it from the CDN or bundle it from npm.
<!-- CDN — one tag in your game's index.html -->
<script src="https://edu.games/sdk/edugames-sdk.v1.js"></script>// npm alternative: npm install @edugames/sdk, then: import EduGames from '@edugames/sdk'
const client = await EduGames.init({ game: 'your-game-slug' });
// client is null only outside any edu.games player (e.g. your local file://)
client.launched(); // once per load (Core)
client.attempted('fractions-1'); // attempt starts (Core)
client.passed('fractions-1', { // objective outcome
score: 0.85, // proportion of attainable performance
durationSeconds: 47, // Timed level
confidence: 0.6, // Confidence level (pre-reveal)
});
client.failed('fractions-2', { score: 0.2 });
client.retried('fractions-2'); // alongside the new attempted()
client.askedForHint('fractions-2');
client.chose('crossroads', 'left', { sequenceIndex: 4 }); // Choice level
client.experienced('tutorial', 'Tutorial screen'); // genuine content exposure
client.logged('powerup-collected'); // telemetry side channel
client.completed({ score: 0.9, durationSeconds: 310 });
// Abandonment is automatic: closing mid-session emits terminated for you.For simple games, a small wrapper around fetch is all you need. No npm dependencies, works in any browser.
// lrs.js — drop this file into your game
const LRS_URL = 'https://lrs.edu.games/xapi';
const AUTH = 'Basic ' + btoa('YOUR_KEY:YOUR_SECRET');
const HEADERS = {
'Authorization': AUTH,
'Content-Type': 'application/json',
'X-Experience-API-Version': '1.0.3',
};
export async function sendStatement(statement) {
const res = await fetch(`${LRS_URL}/statements`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ id: crypto.randomUUID(), ...statement }),
});
if (!res.ok && res.status !== 204) throw new Error(`LRS ${res.status}`);
}
export async function saveState(activityId, agent, stateId, data) {
const params = new URLSearchParams({ activityId, agent: JSON.stringify(agent), stateId });
await fetch(`${LRS_URL}/activities/state?${params}`, {
method: 'PUT', headers: HEADERS, body: JSON.stringify(data),
});
}
export async function loadState(activityId, agent, stateId) {
const params = new URLSearchParams({ activityId, agent: JSON.stringify(agent), stateId });
const res = await fetch(`${LRS_URL}/activities/state?${params}`, { headers: HEADERS });
return res.status === 200 ? res.json() : null;
}The official ADL xAPI wrapper. Handles auth, the version header, and statement batching automatically. Works in browsers and Node.js.
npm install xapiwrapperimport { XAPIWrapper } from 'xapiwrapper';
const lrs = new XAPIWrapper({
endpoint: 'https://lrs.edu.games/xapi/',
user: 'YOUR_KEY',
password: 'YOUR_SECRET',
});
// Send a statement
lrs.sendStatement({
actor: { mbox: 'mailto:student@school.edu', objectType: 'Agent' },
verb: { id: 'http://adlnet.gov/expapi/verbs/completed', display: { 'en-US': 'completed' } },
object: { id: 'https://your-game.com/level-1', objectType: 'Activity' },
result: { score: { raw: 85, min: 0, max: 100, scaled: 0.85 }, completion: true },
}, (response) => {
console.log('Statement ID:', response.responseText);
});
// Save state
lrs.sendState(
{ id: 'https://your-game.com', objectType: 'Activity' },
{ mbox: 'mailto:student@school.edu', objectType: 'Agent' },
null, 'progress',
{ level: 5, score: 1240 }
);
// Load state
lrs.getState(
'https://your-game.com',
{ mbox: 'mailto:student@school.edu', objectType: 'Agent' },
'progress', null, null,
(response) => {
const progress = JSON.parse(response.responseText);
console.log('Current level:', progress.level);
}
);Mature Tin Can / xAPI library from Rustici Software. Well-suited to Phaser and other game frameworks.
npm install tincanjsconst lrs = new TinCan.RemoteLRS({
endpoint: 'https://lrs.edu.games/xapi/',
username: 'YOUR_KEY',
password: 'YOUR_SECRET',
});
lrs.saveStatement(
new TinCan.Statement({
actor: new TinCan.Agent({ mbox: 'mailto:student@school.edu', name: 'Jane' }),
verb: new TinCan.Verb({
id: 'http://adlnet.gov/expapi/verbs/completed',
display: { 'en-US': 'completed' },
}),
target: new TinCan.Activity({
id: 'https://your-game.com/level-1',
definition: { name: { 'en-US': 'Level 1' } },
}),
result: new TinCan.Result({
score: new TinCan.Score({ raw: 85, min: 0, max: 100, scaled: 0.85 }),
completion: true,
success: true,
}),
}),
(err, result) => {
if (err) console.error('LRS error:', err);
else console.log('Saved:', result.id);
}
);Hook xAPI calls into Phaser’s scene lifecycle events for automatic statement tracking.
// In your Phaser scene
import { sendStatement, saveState, loadState } from './lrs.js';
const GAME_ID = 'https://your-game.com';
const LEVEL_ID = (n) => `${GAME_ID}/levels/${n}`;
class LevelScene extends Phaser.Scene {
constructor() { super('LevelScene'); }
async create() {
this.startTime = Date.now();
const actor = { objectType: 'Agent', mbox: `mailto:${this.registry.get('playerEmail')}` };
// Load saved progress
this.progress = await loadState(GAME_ID, actor, 'progress') ?? { level: 1, score: 0 };
// Track that the player started this level
await sendStatement({
actor,
verb: { id: 'http://adlnet.gov/expapi/verbs/attempted', display: { 'en-US': 'attempted' } },
object: { objectType: 'Activity', id: LEVEL_ID(this.progress.level) },
});
}
async onLevelComplete(score, maxScore) {
const actor = { objectType: 'Agent', mbox: `mailto:${this.registry.get('playerEmail')}` };
const elapsed = Math.round((Date.now() - this.startTime) / 1000);
await sendStatement({
actor,
verb: { id: 'http://adlnet.gov/expapi/verbs/completed', display: { 'en-US': 'completed' } },
object: { objectType: 'Activity', id: LEVEL_ID(this.progress.level) },
result: {
score: { raw: score, min: 0, max: maxScore, scaled: score / maxScore },
success: score / maxScore >= 0.7,
completion: true,
duration: `PT${elapsed}S`,
},
});
this.progress.level++;
this.progress.score += score;
await saveState(GAME_ID, actor, 'progress', this.progress);
}
}Before publishing, open your game in the developer sandbox from your dev-portal dashboard: it runs your uploaded package on edugames.dev as a test student, with a live conformance-checked statement feed beside the game. Sandbox statements never reach real analytics.