What is xAPI (Tin Can API)? A plain-language guide to statements and the LRS

What is xAPI? A protocol, not a package: content posts actor–verb–object statements to a learning record store. Statements, LRS, launch, xAPI vs SCORM.

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"
}
actor mbox / account verb IRI + display object activity IRI + + REQUIRED result score success duration context registration parent OPTIONAL Read as a sentence: "Dana Whitfield passed Final assessment — scaled 0.84, in 4 m 12 s, during registration 6c1e…"
Every statement is the three required parts read left to right; result and context hang off it.

Several things in that example are rules an LRS must reject a statement for breaking:

  • The actor must carry exactly one identifier: mbox (a mailto: address), mbox_sha1sum, openid, or an account object with a homePage and name. LMS launches almost always use account, 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; display is 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 (PT4M12S is four minutes and twelve seconds), not seconds.
  • The LRS adds a stored timestamp, an authority, and a UUID id if you did not supply one. A stored statement is immutable: you cannot edit it, only void it with a new statement whose verb is http://adlnet.gov/expapi/verbs/voided and whose object is a StatementRef to 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.

  1. 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.
  2. 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":…}&registration=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.
  3. 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 → … → terminated statement sequence is required. If you want xAPI data and an LMS import, that is the route; see What is cmi5.
"Does your LMS support xAPI?" is not a precise question. It may have an LRS built in, accept a cmi5 package, launch a bare URL with the query-string convention, or simply forward SCORM results as statements. Each is a different capability. Ask which of the four it does before you build.

xAPI vs SCORM

SCORM LMS browser window SCO iframe SetValue API_1484_11 JS object in parent window LMS database: one cmi.* record per attempt xAPI any page, app, or device content no API object HTTPS LRS anywhere, any organisation Store: append-only statements, one per event Each POST is its own commit
SCORM reports into the window that launched it; xAPI reports over the network to a store.

The same events side by side — what a SCORM 2004 course writes through the runtime API, and what an xAPI course posts instead:

EventSCORM 2004 runtime callxAPI equivalent
Course openedInitialize("")Statement with verb …/verbs/initialized (or launched, sent by the launcher)
ScoreSetValue("cmi.score.scaled", "0.84")result.score.scaled: 0.84 on a passed/failed statement
Pass or failSetValue("cmi.success_status", "passed")Verb passed plus result.success: true
FinishedSetValue("cmi.completion_status", "completed")Verb completed plus result.completion: true
BookmarkSetValue("cmi.suspend_data", "…"), ≤ 64,000 charsState document via PUT /activities/state, no cap in the spec
Time on taskSetValue("cmi.session_time", "PT4M12S")result.duration: "PT4M12S"
One quiz answercmi.interactions.0.*, five or six elements, often discarded by the LMSOne answered statement with result.response and the activity's interactionType
PersistCommit("") — the LMS decides when it landsEvery POST is its own commit; the 200 is the receipt
CloseTerminate("")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.

Keep reading

How-to4 min Convert SCORM to video or PowerPoint? Can you convert SCORM to MP4, HTML5 or PowerPoint? What is inside a package, the routes that work, and what you lose with each one. Explainer4 min Does your LMS support xAPI? How to check What xAPI LMS support really means: launching xAPI content, a built-in LRS, or forwarding statements. The questions to ask and a quick test to run. Explainer4 min LMS vs LRS: what is the difference? LMS vs LRS: an LMS runs courses and learners; an LRS stores xAPI statements. Why SCORM does not use an LRS, and when you need both.

Start with one file.

Upload something you already have and see it as a package, with the conformance report and the runtime log alongside it.

→ upload  handbook.pdf
  parsing … 14 sections
  building manifest …
  validating … 0 errors, 2 notes
✓ handbook-scorm2004.zip
✓ preview link ready