xAPI in one paragraph
So, what is xAPI? The Experience API — still widely called Tin Can API, the name of the 2011 project that produced it — is a specification for recording things a person did as short JSON records called statements and sending them over HTTP to a database called a learning record store (LRS). Each statement has the shape actor – verb – object: "Dana passed the final assessment". There is no zip file, no manifest, and no JavaScript API object for the content to find. xAPI 1.0 was published by ADL in 2013, revised as 1.0.3 in 2016, and a 2.0 revision became IEEE 9274.1.1 in 2023; almost everything in production speaks 1.0.3, and every request says so in a header: X-Experience-API-Version: 1.0.3.
If you have read What is SCORM, the contrast is the point: SCORM is a package the LMS imports plus a fixed set of calls to the LMS. xAPI is a protocol the content speaks to a store, from wherever it is running.
The shape of a statement
A statement is a JSON object. Three fields are mandatory — actor, verb, object — and two optional ones, result and context, carry what most reports care about. A complete, valid statement for a learner passing a quiz:
{
"actor": {
"objectType": "Agent",
"name": "Dana Whitfield",
"mbox": "mailto:dana@example.org"
},
"verb": {
"id": "http://adlnet.gov/expapi/verbs/passed",
"display": { "en-US": "passed" }
},
"object": {
"objectType": "Activity",
"id": "https://example.org/courses/fire-safety/final-assessment",
"definition": {
"name": { "en-US": "Final assessment" },
"type": "http://adlnet.gov/expapi/activities/assessment"
}
},
"result": {
"score": { "scaled": 0.84, "raw": 21, "min": 0, "max": 25 },
"success": true,
"completion": true,
"duration": "PT4M12S"
},
"context": {
"registration": "6c1e8f0a-2d4b-4b7e-9a3f-0f5b1c2d3e4f",
"contextActivities": {
"parent": [ { "id": "https://example.org/courses/fire-safety" } ]
}
},
"timestamp": "2026-08-26T09:14:03Z"
}
Several things in that example are rules an LRS must reject a statement for breaking:
- The actor must carry exactly one identifier:
mbox(amailto:address),mbox_sha1sum,openid, or anaccountobject with ahomePageandname. LMS launches almost always useaccount, because they have a user id rather than an email they are willing to expose. - The verb id is an IRI — a URL-shaped identifier — not the word "passed". ADL publishes the common ones under
http://adlnet.gov/expapi/verbs/(completed, passed, failed, attempted, experienced, answered, launched, initialized, terminated). Two systems only agree a learner "passed" if they used the same IRI;displayis for humans and carries no meaning to the store. - The object is usually an Activity identified by an IRI you invent and must keep stable. Change it and every earlier statement about the same quiz is now about something else.
- result.score.scaled is a number between −1 and 1; duration is ISO 8601 (
PT4M12Sis four minutes and twelve seconds), not seconds. - The LRS adds a
storedtimestamp, anauthority, and a UUIDidif you did not supply one. A stored statement is immutable: you cannot edit it, only void it with a new statement whose verb ishttp://adlnet.gov/expapi/verbs/voidedand whose object is aStatementRefto the old id.
What a learning record store does
An LRS is a web service that implements the xAPI REST resources. The one you use most is /statements. Posting the statement above looks like this:
POST /xapi/statements HTTP/1.1
Host: lrs.example.org
Authorization: Basic a2V5OnNlY3JldA==
Content-Type: application/json
X-Experience-API-Version: 1.0.3
{ ...the statement... }
HTTP/1.1 200 OK
["9f1e3b7c-2b5a-4b0a-8d3b-5e6f7a8b9c0d"]
The response is the list of ids the store assigned. Reading back is a GET on the same resource with filters — agent, verb, activity, registration, since, until, limit — answered with a page of statements plus a more URL. The specification defines those filters and nothing richer: there is no "average score by cohort" query in xAPI. Aggregation and dashboards are built on top of the store.
The store also holds documents that are not statements. The State resource (/activities/state) keys an arbitrary document by activity, agent, and optional registration — this is where an xAPI course keeps its bookmark, the equivalent of SCORM's cmi.suspend_data, with no 4 KB or 64 KB cap in the specification. What is a learning record store covers the other resources, querying, forwarding, and whether you need one at all.
How an xAPI activity is launched
This is the part that surprises people coming from SCORM: the xAPI specification says nothing about launch. Content that speaks xAPI is just a URL; something has to tell it where the LRS is, how to authenticate, and who the learner is. In practice that "something" is one of three things.
- Configuration baked into the content. Endpoint and credentials in the code. Fine for an internal app; unacceptable for anything a browser can view-source, because the key is now public.
- Query-string launch. The common convention, from the Tin Can launch guidelines, appends the connection to the URL:
index.html?endpoint=https://lrs.example.org/xapi/&auth=Basic%20a2V5…&actor={"account":…}®istration=6c1e…&activity_id=https://example.org/courses/fire-safety. It works, but the credential travels in a URL that lands in browser history and proxy logs, and nothing defines what the content must send when it opens or closes. - cmi5. A separate specification that adds a package, a manifest, and a defined launch handshake: the LMS passes a one-time fetch URL instead of a credential, the content exchanges it for a session token, and a fixed
launched → initialized → … → terminatedstatement sequence is required. If you want xAPI data and an LMS import, that is the route; see What is cmi5.
xAPI vs SCORM
The same events side by side — what a SCORM 2004 course writes through the runtime API, and what an xAPI course posts instead:
| Event | SCORM 2004 runtime call | xAPI equivalent |
|---|---|---|
| Course opened | Initialize("") | Statement with verb …/verbs/initialized (or launched, sent by the launcher) |
| Score | SetValue("cmi.score.scaled", "0.84") | result.score.scaled: 0.84 on a passed/failed statement |
| Pass or fail | SetValue("cmi.success_status", "passed") | Verb passed plus result.success: true |
| Finished | SetValue("cmi.completion_status", "completed") | Verb completed plus result.completion: true |
| Bookmark | SetValue("cmi.suspend_data", "…"), ≤ 64,000 chars | State document via PUT /activities/state, no cap in the spec |
| Time on task | SetValue("cmi.session_time", "PT4M12S") | result.duration: "PT4M12S" |
| One quiz answer | cmi.interactions.0.*, five or six elements, often discarded by the LMS | One answered statement with result.response and the activity's interactionType |
| Persist | Commit("") — the LMS decides when it lands | Every POST is its own commit; the 200 is the receipt |
| Close | Terminate("") | Statement with verb terminated |
Three differences do the real work. First, where the data goes: SCORM writes to the LMS that launched the course and nowhere else; xAPI writes to whichever store the content was pointed at, which may belong to a different organisation than the platform hosting the page. Second, what can be recorded: SCORM has a fixed data model (cmi.*) and anything outside it does not exist; a statement can describe any actor doing any verb to any object, so "rewatched chapter 3" or "chose branch B" are first-class records rather than something crammed into suspend data. Third, the failure mode: a SCORM completion is lost when the final Commit never lands, as in why SCORM completions go missing; a statement is lost when the HTTP request fails, and the content knows immediately because it did not get a 200.
What xAPI does not give you is any of SCORM's LMS-side behaviour: no import, no catalogue entry, no completion column, and no notion of an attempt unless your reporting derives one from registration. The three-way comparison, including cmi5, is in SCORM vs cmi5 vs xAPI.
When xAPI is the wrong choice
xAPI is the right tool when content runs outside an LMS, when you need per-interaction or per-path data, or when several systems must contribute to one learner record. It is the wrong tool in four common situations.
- The destination only imports packages. If the customer's LMS accepts a zip and reports completion from it, and that is all they asked for, xAPI adds an LRS they do not have and a reporting layer they will not build. Ship SCORM.
- Nobody owns the LRS. Statements need a store, credentials, retention rules, and someone to answer "what does this dashboard mean". Without an owner the data accumulates and is never read.
- Your identifiers are not stable. Actor and activity IRIs are the join keys for everything. If learner accounts are re-created on every LMS migration, or activity ids change per build, the statements cannot be related to each other.
- You want fewer failure modes, not more. A SCORM course has one connection to debug (the API object). An xAPI course has an endpoint, a credential, CORS, and network reachability from the learner's browser to the LRS, any of which fails silently if the content does not check the response code.
A reasonable default for LMS-delivered training is still SCORM for the record the LMS needs, with xAPI statements added where a specific report requires them — and a cmi5 package when you need both in one import.
Where SCORM Central fits
SCORM Central converts files you already have — PDF, PowerPoint, Word, or video — into SCORM 1.2, SCORM 2004, cmi5, or xAPI, and shows you the manifest, the full runtime log, and a conformance report on every build, so you can see exactly what a package will report before an LMS does. See how it works, or start with one file.