Documentation
RESOURCES

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.

@edugames/sdk (official)
Recommended

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.

html
<!-- CDN — one tag in your game's index.html -->
<script src="https://edu.games/sdk/edugames-sdk.v1.js"></script>
javascript
// 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.
Vanilla fetch (no dependencies)
Recommended for small games

For simple games, a small wrapper around fetch is all you need. No npm dependencies, works in any browser.

javascript
// 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.

bash
npm install xapiwrapper
javascript
import { 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.

bash
npm install tincanjs
javascript
const 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);
  }
);
Phaser 3 Integration

Hook xAPI calls into Phaser’s scene lifecycle events for automatic statement tracking.

javascript
// 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);
  }
}
Test on edugames.dev

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.

Activities APIFAQ