Documentation
xAPI INTEGRATION

Tracking Progress

The xAPI State API lets you persist and restore arbitrary learner data — level progress, settings, bookmarks — across devices and sessions. It works independently of statements.

How State Works

Each state document is stored against a unique combination of activityId + agent + stateId. You can have multiple state IDs per agent+activity — e.g. progress, settings, bookmarks.

Saving State

PUT replaces the document entirely. Send JSON (or any content type) as the request body.

javascript
const agent = JSON.stringify({ objectType: 'Agent', mbox: 'mailto:student@school.edu' });

const params = new URLSearchParams({
  activityId: 'https://your-game.com',
  agent,
  stateId: 'progress',
});

await fetch(`https://lrs.edu.games/xapi/activities/state?${params}`, {
  method: 'PUT',
  headers: {
    'Authorization': 'Basic ' + btoa('YOUR_KEY:YOUR_SECRET'),
    'Content-Type': 'application/json',
    'X-Experience-API-Version': '1.0.3',
  },
  body: JSON.stringify({
    currentLevel: 5,
    totalScore: 1240,
    completedLevels: [1, 2, 3, 4],
    unlockedAchievements: ['first-blood', 'speedrun'],
    lastSaved: new Date().toISOString(),
  }),
});
// Response: 204 No Content

Loading State

javascript
async function loadProgress(learnerEmail, gameId) {
  const agent = JSON.stringify({ objectType: 'Agent', mbox: `mailto:${learnerEmail}` });
  const params = new URLSearchParams({ activityId: gameId, agent, stateId: 'progress' });

  const res = await fetch(`https://lrs.edu.games/xapi/activities/state?${params}`, {
    headers: {
      'Authorization': 'Basic ' + btoa('YOUR_KEY:YOUR_SECRET'),
      'X-Experience-API-Version': '1.0.3',
    },
  });

  if (res.status === 200) {
    return await res.json();
    // { currentLevel: 5, totalScore: 1240, ... }
  }

  if (res.status === 404) {
    return null; // No saved progress — start fresh
  }

  throw new Error(`Failed to load progress: ${res.status}`);
}

Multiple State Documents

Use separate state IDs for logically distinct data. This keeps documents small and avoids merge conflicts.

javascript
// Save game progress separately from user preferences
await saveState('progress', {
  currentLevel: 5,
  totalScore: 1240,
  completedLevels: [1, 2, 3, 4],
});

await saveState('settings', {
  soundEnabled: true,
  difficulty: 'medium',
  colorblindMode: false,
});

// List all state IDs for this agent+activity:
const params = new URLSearchParams({
  activityId: 'https://your-game.com',
  agent: JSON.stringify({ objectType: 'Agent', mbox: 'mailto:student@school.edu' }),
});

const res = await fetch(`https://lrs.edu.games/xapi/activities/state?${params}`, {
  headers: { 'Authorization': 'Basic ' + btoa('YOUR_KEY:YOUR_SECRET'), 'X-Experience-API-Version': '1.0.3' },
});
const ids = await res.json(); // ["progress", "settings"]

Deleting State

javascript
const agent = JSON.stringify({ objectType: 'Agent', mbox: 'mailto:student@school.edu' });

// Delete a single state document
const params = new URLSearchParams({
  activityId: 'https://your-game.com',
  agent,
  stateId: 'progress',
});
await fetch(`https://lrs.edu.games/xapi/activities/state?${params}`, {
  method: 'DELETE',
  headers: { 'Authorization': 'Basic ' + btoa('YOUR_KEY:YOUR_SECRET'), 'X-Experience-API-Version': '1.0.3' },
});

// Delete ALL state documents for this agent+activity (e.g. "reset game")
const allParams = new URLSearchParams({ activityId: 'https://your-game.com', agent });
await fetch(`https://lrs.edu.games/xapi/activities/state?${allParams}`, {
  method: 'DELETE',
  headers: { 'Authorization': 'Basic ' + btoa('YOUR_KEY:YOUR_SECRET'), 'X-Experience-API-Version': '1.0.3' },
});

Querying a Learner’s Statement History

Use the Statements API to reconstruct a learner’s history — useful for showing a progress summary or debugging.

javascript
const params = new URLSearchParams({
  agent: JSON.stringify({ objectType: 'Agent', mbox: 'mailto:student@school.edu' }),
  activity: 'https://your-game.com',
  limit: '50',
  ascending: 'true', // oldest first
});

const res = await fetch(`https://lrs.edu.games/xapi/statements?${params}`, {
  headers: {
    'Authorization': 'Basic ' + btoa('YOUR_KEY:YOUR_SECRET'),
    'X-Experience-API-Version': '1.0.3',
  },
});

const { statements, more } = await res.json();
// statements: array of xAPI statement objects, oldest first
// more: path to fetch next page (empty string if no more)

// Filter completed statements to build a level history
const completed = statements.filter(s =>
  s.verb.id === 'http://adlnet.gov/expapi/verbs/completed'
);

Recommended Integration Pattern

The typical lifecycle for a game session:

1
Game loads
Call GET state with stateId "progress". If 200, restore that state. If 404, start fresh.
2
Session begins
Generate a registration UUID. Send an "attempted" statement for the game/level with this registration.
3
During play
Auto-save state every minute or at each checkpoint (PUT state). Send "answered" statements for each question.
4
Level complete
Send "completed" (or "passed"/"failed") statement with result including score and duration.
5
Game closes
Final PUT state with latest progress. Optionally send a "terminated" statement.

Privacy Considerations

Actor identification: State documents and statements are indexed by the agent object you provide. Use the account form (with an opaque user ID) instead of mbox if you do not want to store learner email addresses in the LRS.
Sending StatementsEmission Profile