Documentation
xAPI INTEGRATION

Emission Profile

The edu.games xAPI Emission Profile v1 defines the statement shapes your game should emit to power the platform’s Learning Intelligence visualisations. Games that follow more of this profile unlock more presets for their buyers — the conformance level is shown on your game’s listing as a sales incentive, not a gate to publish. You can publish a game with zero xAPI traffic; following this profile just makes it more attractive to teachers shopping for data-backed games.

Levels

Each level is additive — Timed, Choice, Confidence, and Full all assume Core is already in place. The higher the level, the more preset visualisations light up on the game’s insights dashboard.

LevelRequirementsUnlocks
Core
(required to list analytics at all)
actor echo, launched/attempted/passed/failed verbs, object IRIs per objectiveP02 P03 P06* P07 P08 P11–P13 P15 P16 P18–P20, radar/mastery (AI infers from core verbs)
Timed+ result.duration per attemptP05, P17 (accurate)
Choice+ decision statementsP04, P14
Confidence+ confidence extensionP10
Fullall of the above + state + hintseverything, "Full Learning Intelligence" badge

* P06 needs hint/retry verbs — part of Full, degraded otherwise.

1. Actor Echo (Core)

The player delivers a launch context to your game — query params for HTML games, window.EDUGAMES_LAUNCH for embedded games, or a { type: 'edugames:launch' } postMessage for iframe games:

json
{
  "actor": { "objectType": "Agent",
             "account": { "homePage": "https://edu.games", "name": "<opaque-student-id>" } },
  "registration": "<uuid>",
  "endpoint": "https://lrs.edu.games/xapi",
  "auth": "Bearer <launch-token>"
}

Hard rules:

  • Games MUST use this actor and registration verbatim in every statement (context.registration).
  • Never invent actors; never put student names or emails in statements. Anonymous play (no actor delivered) uses an account.name prefixed anon: so it can never collide with real student ids.
  • Tag every statement with the profile version in context.contextActivities.category:
json
{ "id": "https://edu.games/xapi/profiles/emission/v1",
  "definition": { "type": "http://adlnet.gov/expapi/activities/profile" } }

1a. Launch-Channel Security (mandatory)

The postMessage channel is spoofable by other frames — a hostile frame could otherwise redirect student data to an attacker’s endpoint. Your game MUST:

  • accept edugames:launch only when event.source === window.parent;
  • treat the first launch context as final — ignore later launch messages;
  • never let a postMessage override an endpoint/auth already set by the injected globals (messages may only fill in missing actor/registration);
  • POST only to the endpoint obtained from the launch context — never one taken from any other message or parameter;
  • allowlist postMessage endpoints: a hostile page embedding your game is itself window.parent, so an endpoint received via postMessage must be https: with host edu.games, ending in .edu.games, or edugames.dev (the developer sandbox). A message with a non-allowlisted endpoint is rejected in its entirety — take nothing from it, including actor and registration (an embedder that injects an evil endpoint can’t be trusted for identity either); keep waiting for a valid launch or fall back to anonymous. Endpoints from the injected globals are exempt (only the real player can inject those).

Player guarantee: the edu.games player injects the globals into <head> before any game script runs. Treat the globals as the primary channel and postMessage as a fallback for other embedders — inside the player, the wait-for-postMessage path never executes.

1b. Delivery & Attribution (mandatory)

  • The player’s game frame (served by /api/play-frame, sandboxed without allow-same-origin) has an opaque origin — storage APIs throw. Queue statements in memory; flush every 5–10 s and on pagehide/visibilitychange: hidden with fetch(..., { keepalive: true }), capping the final batch at ~20 statements / under 60 KB (browsers cap keepalive bodies at ~64 KB).
  • Never re-attribute statements. Queued or persisted statements keep the actor and registration they were created with — re-stamping a backlog with a newly arrived launch context misattributes data on shared classroom devices. Discard persisted statements with no actor.
  • Late launch context: hold statements (with true timestamps) until the launch resolves — injected globals immediately, else wait ~3 s for the postMessage, else anonymous fallback. Never switch actors mid-session. (Inside the player the globals are guaranteed, so this path is fallback-only.)
  • Cross-load persistence is not expected — the sandbox makes it impossible, and the conformance checker does not penalize its absence. In-memory queue + regular flushes is full compliance.
  • Anonymous identity is per-load: a fresh anon:<uuid> actor per page load is expected and compliant. Do not build your own visitor tracking to make anon ids stable — if per-visitor anonymous aggregation is ever needed, the player will inject a stable visitor token (planned).
  • Anonymous registration: anonymous sessions MUST carry a game-generated UUID v4 in context.registration, one per session. The conformance checker treats a non-platform registration as valid when the actor is anon:-prefixed.
  • LRS retry guarantee: lrs.edu.games dedupes by statement id — resubmitting a batch containing already-stored ids succeeds (no batch-level conflict), so retry the identical batch, ids included, and your retry loop is guaranteed to drain.

2. Verbs (Core)

Use these exact IRIs:

MeaningIRI
session starthttp://adlnet.gov/expapi/verbs/launched
attempt starthttp://adlnet.gov/expapi/verbs/attempted
objective successhttp://adlnet.gov/expapi/verbs/passed
objective failurehttp://adlnet.gov/expapi/verbs/failed
content exposurehttp://adlnet.gov/expapi/verbs/experienced
completionhttp://adlnet.gov/expapi/verbs/completed
masteryhttps://w3id.org/xapi/dod-isd/verbs/mastered
hint requestedhttps://edu.games/xapi/verbs/asked-for-hint
retryhttps://edu.games/xapi/verbs/retried
decision madehttps://edu.games/xapi/verbs/chose
session/attempt abandonedhttp://adlnet.gov/expapi/verbs/terminated
non-learning telemetryhttps://edu.games/xapi/verbs/logged

Abandonment: quitting mid-game is terminated — never completed with success: false (that reads as a failed completion). Emit it from your final-flush handler when the session ends before completion.

Session semantics: one session = one launch context. Emit exactly one launched per load and keep one registration throughout, even across “Play Again” replays. Each play is bracketed by its first attempted and ends with completed (finished) or terminated (abandoned). Never re-emit launched or mint a new registration per play. A load that never starts a play (title-screen bounce) emits no terminatedlaunched with nothing after it is the bounce signal.

3. Objects / Objectives (Core)

Every learning objective is an Activity with a stable IRI:

text
https://edu.games/games/<game-slug>/objectives/<objective-slug>

with definition.name and definition.type = http://adlnet.gov/expapi/activities/objective. These IRIs are exactly what you map to competencies in the dev portal, so keep them stable once you ship them.

3a. Non-Learning Telemetry (side channel)

Gameplay signals that are neither objectives nor content — hazard collisions, power-ups, deaths — use verb https://edu.games/xapi/verbs/logged with objects under the reserved namespace https://edu.games/games/<game-slug>/events/<event-slug>. These are stored but ignored by all presets, so log freely. Do not use experienced for telemetry — it is reserved for genuine content exposure (screens, tutorials, media), and telemetry sent under it pollutes exposure analytics.

4. Results

  • result.success (boolean) on passed/failed.
  • result.score.scaled (−1..1) where scoring exists. Semantics: proportion of attainable performance on the attempt (fraction correct, or raw/max with a defined per-objective max, clamped). Never raw points, and never collapse to 1/0 when a finer proportion exists — this number is compared across games. When no finer proportion exists, 1/0 is correct: a single multiple-choice question is simply 1.0 right / 0.0 wrong — do not invent granularity (speed bonuses, streaks) to look continuous.
  • Timed: result.duration as an ISO8601 duration (e.g. PT47S) per attempt — active time only (pause the timer while paused or hidden).
  • Confidence: result.extensions["https://edu.games/xapi/ext/confidence"] — a number 0..1, captured from the learner before revealing correctness. Only for games with a natural pre-reveal moment; real-time/action games skip this tier rather than inserting a dialog.
  • Retry semantics: retried fires when a new attempt starts at an objective already failed this session (any later attempt), alongside that attempt’s attempted statement — never instead of it, never across sessions.
  • Mastery default: 3 consecutive passes at an objective, unless the game has an explicit built-in mastery mechanic.

5. Decision Statements (Choice)

Emit one statement per branch point, using the chose verb:

json
{ "verb": { "id": "https://edu.games/xapi/verbs/chose" },
  "object": { "id": "https://edu.games/games/<slug>/decisions/<point>/<choice>",
              "definition": { "type": "http://adlnet.gov/expapi/activities/choice" } },
  "context": { "registration": "<launch registration>",
    "extensions": {
      "https://edu.games/xapi/ext/decision-point": "<point-slug>",
      "https://edu.games/xapi/ext/sequence-index": 4,
      "https://edu.games/xapi/ext/parent-point": "<previous-point-slug|null>"
    } } }

The chosen option is extracted from the last path segment of the object IRI, so point and choice slugs must match [a-z0-9-]+ (a / in a slug corrupts extraction). sequence-index is 0-based per session.

6. State Statements (Full)

On objective state change, emit the completed/mastered verbs, or set context.extensions["https://edu.games/xapi/ext/state"] to one of not_started, in_progress, completed, mastered.

7. Checking Your Conformance

Once your game has an edu.games credential, head to dev.edu.games → your game → Analytics Readiness to check how your emitted statements measure up against this profile:

  • A live statement inspector — the last 50 statements from your game’s credential, parsed against this profile with per-statement pass/warn/fail annotations (e.g. “actor is not the platform actor”, “duration missing on passed”).
  • Capability flags as observed nightly, each shown with the preset list it unlocks, greyed out until data arrives and lit once it does.
  • A test-harness page that launches your game with a sandbox credential and a fake student, so you can watch statements arrive in real time before you publish.

Presets check saved_visualizations.requires ⊆ game_capabilities. If your game hasn’t declared a capability, the corresponding chart renders greyed out with “Requires decision tracking — this game hasn’t declared it. See Emission Profile.” instead of an empty chart.

Amendments — 27 July 2026

The profile was amended after the first full game instrumentation surfaced gaps in v1 as originally published. If you integrated before this date, review the following — all are live in the platform now:

  • Security (§1a, new): launch messages accepted only from window.parent, first launch wins, and postMessage-delivered endpoints must be on edu.games/*.edu.games/edugames.dev — a message with a non-allowlisted endpoint is rejected in its entirety, identity included.
  • Delivery & attribution (§1b, new): in-memory queueing (the sandbox has no storage), capped keepalive final flush, never re-attribute queued statements, anonymous anon: actors with game-minted UUID registrations, and a documented LRS dedupe guarantee for retries.
  • Profile version (§1): every statement now carries https://edu.games/xapi/profiles/emission/v1 in context.contextActivities.category.
  • New verbs (§2): terminated for abandonment (its own analytics class — never send completed with success: false for a quit) and logged for non-learning telemetry under the /events/ namespace (§3a).
  • Session semantics (§2): one launched and one registration per launch context, plays bracketed by attemptedcompleted/terminated; a title-screen bounce emits no terminated.
  • Comparability (§4): score.scaled defined as proportion-of-attainable (with the single-question 1/0 case spelled out), duration is active time, retry and mastery semantics pinned, confidence scoped to games with a natural pre-reveal moment.
  • Decision slugs (§5): constrained to [a-z0-9-]+; sequence-index is 0-based.
Tracking ProgressInstrumenting with Claude Code