What is the structure of an xAPI statement?
An xAPI statement is a JSON object with three required fields — actor, verb, object — plus optional result, context, timestamp and id. The xAPI statement structure reads as a sentence: someone did something to something. Everything else qualifies that sentence with a score, a duration, or the attempt it belonged to.
That is the whole model, and it is why xAPI can describe a quiz, a video, a simulator and a classroom sign-in with the same shape. The difficulty is never the shape: several fields carry identifiers that two systems must agree on exactly, and a report that looks empty is usually a disagreement about one of them rather than missing data. A complete statement, with every field this article covers:
{
"id": "9f6b1a34-7c02-4e58-9d21-1b0a7e4d55c9",
"actor": {
"objectType": "Agent",
"name": "Dana Whitfield",
"account": {
"homePage": "https://lms.example.org",
"name": "u-40219"
}
},
"verb": {
"id": "http://adlnet.gov/expapi/verbs/passed",
"display": { "en-GB": "passed" }
},
"object": {
"objectType": "Activity",
"id": "https://example.org/courses/fire-safety/final-assessment",
"definition": {
"name": { "en-GB": "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",
"platform": "Example LMS",
"language": "en-GB",
"contextActivities": {
"parent": [ { "id": "https://example.org/courses/fire-safety" } ]
}
},
"timestamp": "2026-09-08T09:14:03Z"
}
| Field | Required | What it carries |
|---|---|---|
actor | Yes | Who did it — an Agent or a Group, carrying exactly one identifier |
verb | Yes | What they did, as an IRI, with an optional human-readable display |
object | Yes | What they did it to — usually an Activity with a stable IRI |
result | No | Score, success, completion, response and duration |
context | No | Registration, parent and grouping activities, platform, language |
timestamp | No | When the event happened, as reported by the sender |
id | No | A UUID for the statement; the store assigns one if you omit it |
Two more fields appear on statements you read back but never on ones you send: stored, the time the store received it, and authority, the credential that wrote it. If you are new to the model as a whole, what xAPI is and how it differs from SCORM covers the protocol around these statements.
Identifying the actor
The actor is an Agent (one person) or a Group, and an Agent must carry exactly one inverse functional identifier — the field that says which human this is. There are four:
mbox— an email address written as amailto:IRI, such asmailto:dana@example.org. Simple, and it puts an email address in every record you ever store.mbox_sha1sum— the SHA-1 hash of thatmailto:IRI, for when you want matching without storing the address.openid— an OpenID URI.account— an object with ahomePage(the system that issued the id) and aname(the id itself). This is what an LMS launch almost always sends, because the platform has a user id and no reason to expose an email.
Sending two identifiers, or none, makes the statement invalid and the store will reject it. The subtler failure is sending a valid one that nobody else uses. An Agent identified by account on https://lms.example.org and the same person identified by mbox are two different learners as far as any query is concerned, and no amount of reporting configuration will merge them after the fact. Decide the identifier once, per source system, before you send anything you intend to report on.
A Group is either anonymous — no identifier, just a member array of Agents, useful for a team that has no id in any system — or identified, with its own identifier and optionally members. Groups are rare in course data and common in classroom, cohort and team scenarios.
Verbs are IRIs
The verb has one required field, id, and it is an IRI — a URL-shaped identifier — not the English word. display is a language map for humans and carries no meaning at all to the store: two systems agree a learner passed only if both sent http://adlnet.gov/expapi/verbs/passed, whatever their display strings say.
ADL publishes the vocabulary most content uses under http://adlnet.gov/expapi/verbs/, including completed, passed, failed, attempted, experienced, answered, launched, initialized and terminated. One is reserved: voided. Stored statements are immutable, so a mistake is not edited or deleted — it is voided by a new statement whose verb is voided and whose object is a StatementRef pointing at the old statement's id.
You may mint your own verb IRIs on a domain you control, and for genuinely domain-specific actions you should. The cost is that nothing else in the world knows what they mean, so a dashboard built elsewhere will not chart them. Use the published vocabulary for anything that maps onto it, and invent only where nothing fits.
The object and its definition
Most of the time the object is an Activity: an id that is an IRI you invent, plus an optional definition. The id is the single most load-bearing string in your data. Every statement about the same quiz must use the same activity id forever; change it during a content update and your reports show two unrelated activities with the attempts split between them. Choose a scheme that does not encode anything likely to change — not the file name, not the version, not the folder it happened to sit in.
The definition describes the activity for anything that has to render it:
nameanddescription— language maps, keyed by tag such asen-GB.type— an IRI saying what kind of thing this is, for examplehttp://adlnet.gov/expapi/activities/assessmentor.../cmi.interactionfor a single question.moreInfo— a URL a human can open.interactionTypeand its companions (correctResponsesPattern,choices,scale,source,target,steps) for question-level activities. The permitted interaction types aretrue-false,choice,fill-in,long-fill-in,matching,performance,sequencing,likert,numericandother— the same list SCORM 2004 uses, which is not a coincidence.
An object does not have to be an Activity. It can be an Agent or Group ("Dana mentored Ravi"), a StatementRef pointing at another statement (how voiding and commenting work), or a SubStatement, a whole statement embedded as the object to express something that did not happen yet — "Dana was assigned to complete the fire-safety course". A SubStatement is data about an intention; it is not itself a record that the thing occurred.
Result: score, success, duration
The result object is optional and so is every field inside it. That flexibility is the trap: a score with no success, or a completion with no score, is valid, will be stored without complaint, and will then quietly fail whatever rule your report applies.
score.scaled— a number from −1 to 1. This is the field most reporting tools read first, and the only one comparable across activities with different point totals.score.raw,score.min,score.max— the points as marked.rawshould sit betweenminandmax; nothing forces it to, so it is worth checking what your content sends.success— a boolean: did they pass. Separate from completion, exactly as in SCORM 2004, where completion and success are two different fields and conflating them is a classic reporting bug.completion— a boolean: did they finish. A learner can becompletion: true, success: false, which is a finished, failed attempt, and that combination is the one most naive dashboards get wrong.response— what the learner actually answered, as a string, formatted according to the activity's interaction type.duration— ISO 8601, not seconds.PT4M12Sis four minutes twelve seconds;PT1H30Mis ninety minutes.
252 or "4:12" where the specification wants PT4M12S produces an invalid statement that a strict store rejects outright and a lenient one accepts and cannot aggregate. If a time-on-task report reads zero across the board, check the format before you check the query.Context and registration
The context object answers "as part of what?", and the field that matters most is registration: a UUID identifying one attempt at one course by one learner. Every statement in that attempt — launched, answered, answered, answered, passed, terminated — carries the same registration, which is what lets a store reassemble a sequence of statements into a session. Without it you have a pile of events with timestamps and a lot of guessing about which belong together.
contextActivities places the activity in a structure, using four buckets:
parent— the thing this activity is directly part of. A question's parent is the assessment; the assessment's parent is the course.grouping— looser membership, such as the curriculum or programme the course belongs to.category— which profile or specification the statement follows. This is the mechanism a profile uses to mark its own statements so that a store can filter to exactly the ones that count; cmi5 is the widely deployed example, and it is why cmi5 data is queryable in a way that free-form xAPI often is not.other— anything relevant that is none of the above.
The remaining context fields are smaller but useful: platform (a free-text name for the sending system), language (an RFC 5646 tag), revision (the content version), instructor, and extensions for anything the specification does not model, keyed by an IRI you own. platform and revision are only permitted when the object is an Activity.
Get actor, activity id and registration consistent and everything downstream is a query. Get any one of them wrong and the data is technically valid, permanently stored, and useless — which is the practical argument for testing what your content sends against a real store before a cohort runs through it, not after. What a learning record store does covers the receiving side of that test.
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.