Skip to content

The Anatomy of an xAPI Statement

Summary

Building on Chapter 1's vocabulary, this chapter covers the remaining pieces of a statement: sub-statements, attachments, extensions, and the immutability/voiding model that makes the statement log trustworthy. It then turns to the RESTful mechanics of an xAPI endpoint — HTTP verbs, query parameters, and the two authentication schemes an LRS supports.

Concepts Covered

This chapter covers the following 15 concepts from the learning graph:

  1. Sub-Statement
  2. Attachment
  3. Extensions
  4. Statement Immutability
  5. Voided Statement
  6. Registration
  7. Actor Account
  8. Inverse Functional Identifier
  9. Statement ID
  10. RESTful API
  11. HTTP Verb
  12. xAPI Endpoint
  13. Statement Query Parameter
  14. OAuth Authentication
  15. Basic Authentication

Prerequisites

This chapter builds on concepts from:


Back for more record-keeping

Rowan waving welcome Welcome back! Chapter 1 gave you a Statement's core three words — Actor, Verb, Object — plus its two optional add-ons, Result and Context. That's enough to read a Statement. This chapter is about everything else a Statement needs to be trustworthy, traceable, and actually deliverable over the internet. Let's follow the record.

Chapter 1 left off with a complete, human-readable sentence: "Maya completed the Photosynthesis Quiz, scoring 9/10, in Biology 101." That sentence is enough for a person to understand, but it is not yet enough for a computer system to store safely, correct when it is wrong, or retrieve efficiently out of millions of others like it. This chapter fills in the rest of the picture in two stages. First, it covers the parts of a Statement that give it identity and integrity — a unique ID, an unchangeable record, a way to correct mistakes without editing history, and richer optional data like nested statements, file attachments, and custom fields. Second, it covers how a Statement actually travels from a Learning Record Provider to a Learning Record Store: the RESTful HTTP mechanics of the xAPI Endpoint itself.

Giving Every Statement an Identity

Every xAPI Statement carries a Statement ID — a globally unique identifier, formatted as a UUID (Universally Unique Identifier, a 128-bit value written as 32 hexadecimal digits), assigned either by the Learning Record Provider that sends the Statement or, if the Provider leaves it out, by the Learning Record Store that receives it. A Statement ID exists for exactly one reason: so that any Statement, out of potentially billions stored across many textbooks and districts, can be looked up, referenced, or checked for a duplicate without any ambiguity.

That single design choice — giving every Statement a permanent, unique name — is what makes the next concept possible.

Immutability: The Log Only Ever Grows

Statement Immutability is the rule that once a Statement has been accepted by a Learning Record Store, it is never edited and never deleted. If a Learning Record Provider made a mistake — recorded the wrong score, or sent a duplicate — the fix is not to change the original Statement. The fix is to send a new Statement that says, in effect, "ignore that earlier one."

This might feel like an inconvenient rule at first, so it is worth being explicit about why the design insists on it. A record that can be quietly rewritten after the fact cannot be trusted by anyone who did not write it — a teacher reviewing a mastery report, an auditor checking a compliance claim, or a researcher studying whether a new MicroSim improved learning outcomes. Immutability is what turns a Learning Record Store's contents from "data someone typed in" into evidence.

Immutable doesn't mean permanent-and-wrong

Rowan thinking with a magnifying glass Don't confuse "immutable" with "impossible to correct." Immutability only forbids silently changing history. Correcting a mistake openly — by adding a new Statement that explains what changed — is not only allowed, it is the entire point of the mechanism you're about to learn: voiding.

Correcting Mistakes Without Erasing History

A Voided Statement is the formal, spec-defined way to retract an earlier Statement. To void a Statement, a Learning Record Provider sends a brand-new Statement whose Verb is the special reserved verb voided, and whose Object is a StatementRef — a small pointer object that names the Statement ID of the Statement being voided. The Learning Record Store never deletes the original; it simply now has two Statements on file: the original, and a second one that says "the Statement with this ID should no longer count."

Consider Maya's quiz Statement from Chapter 1 again. Suppose the LRS discovers her score was recorded as 9/10 when it should have been 6/10, because of a grading-script bug. The correction is not an edit — it is two new Statements: one voiding the original by its Statement ID, and a second, freshly-IDed Statement recording the correct score of 6/10. All three Statements remain in the log forever; only the current, valid view of Maya's quiz attempt changes, and that view is computed by an LRS query, not by rewriting history.

The following diagram traces this exact sequence — emit, discover an error, void, re-record — end to end.

Diagram: Voiding Lifecycle Flow

Voiding Lifecycle Flow

Type: workflow sim-id: voiding-lifecycle-flow
Library: Mermaid
Status: Specified Template: https://github.com/dmccreary/xapi-course/tree/main/docs/sims/voiding-lifecycle-flow

Bloom Taxonomy: Understand (L2) Bloom Taxonomy Verb: explain, sequence

Learning objective: Let the learner trace the full lifecycle of a mistaken Statement from emission through correction, and see concretely why immutability and voiding are two halves of one mechanism rather than opposing rules.

Purpose: Show an eight-step Mermaid flowchart tracing a Statement's lifecycle when an error is discovered after the fact.

Steps:

  1. "Learning Record Provider emits Statement A" — Maya's quiz Statement, score 9/10, Statement ID abc-123
  2. "Learning Record Store stores Statement A" — accepted, immutable from this point forward
  3. "Error discovered" — grading script bug found, correct score is 6/10
  4. "Provider emits Statement B: voided" — Verb is voided, Object is a StatementRef pointing to abc-123
  5. "LRS stores Statement B" — Statement A is never deleted or edited; both now exist
  6. "LRS flags Statement A as voided" — internal bookkeeping, not a mutation of A's fields
  7. "Provider emits Statement C" — a new, independent Statement with the corrected score of 6/10 and a new Statement ID
  8. "Default queries now return only Statement C" — Statement A remains in the log for audit purposes but is excluded from normal result sets

Interactive features: Every node in the Mermaid flowchart has a click directive. Clicking any node opens an infobox with a one-sentence explanation of that step, and clicking nodes 1, 4, and 7 additionally shows the relevant Statement's Actor/Verb/Object in miniature.

Color coding: Steps 1-2 (original Statement) in the book's teal accent color; steps 3-6 (the voiding action) in amber to signal "correction in progress"; steps 7-8 (the new, correct record) in green to signal "resolved."

Implementation: Mermaid flowchart, top-to-bottom orientation, with full click-to-infobox coverage on every node. Responsive width tracking the containing element.

Two Ways to Identify Who Did It

Chapter 1 defined Actor as the person or agent who performed an experience, but it did not say how a system tells which person that is. xAPI supports more than one way to identify an Actor, and the most common is the Actor Account: a combination of a homePage (the URL of the system that issued the account, such as a district's student information system) and a name (the account identifier within that system, such as a student ID). Together, homePage plus name uniquely identifies one Actor, the same way "checking account number 4471 at First National Bank" is unambiguous even though "account number 4471" alone is not.

Actor Account is one member of a broader family called an Inverse Functional Identifier, or IFI — the umbrella term for any property of an Actor that is guaranteed to identify one and only one person. The xAPI specification defines four kinds of IFI, and every Actor must use exactly one:

IFI Type What It Holds Typical Use
mbox An email address, prefixed mailto: Adult learners with a stable email
mbox_sha1sum A SHA-1 hash of an mbox email Same as mbox, but the raw email is never stored
openid An OpenID URI Learners who authenticate through an OpenID provider
account An Actor Account (homePage + name) District or school-issued student accounts — the most common choice for K-12 systems

Why K-12 systems almost always choose 'account'

Rowan pointing helpfully Younger students often don't have a personal email address, which rules out mbox and mbox_sha1sum. District-issued Actor Accounts, tied to the Student Information System that already manages enrollment, are the practical default for this book's audience — and this project's own LRS is built around exactly that choice.

Statements Inside Statements, and Everything Else Optional

Three more pieces round out a Statement's full shape, each solving a different problem that Actor, Verb, Object, Result, and Context cannot.

A Sub-Statement is a complete Actor-Verb-Object structure nested inside another Statement's Object field, used when one Statement needs to describe or report on another action rather than record it directly. For example, a teacher's Statement might say "Ms. Alvarez (Actor) recommended (Verb) [Maya practiced sine waves] (Sub-Statement as Object)" — the Sub-Statement describes an action without asserting, as a top-level Statement would, that the recommended practice actually happened yet. A Sub-Statement is not itself independently stored or voidable; it only exists nested inside its parent.

An Attachment lets a Statement carry binary data — a PDF certificate, a screenshot, an audio recording of a spoken-language exercise — alongside the Statement itself. Rather than embedding the file's raw bytes, the Statement references the Attachment by a cryptographic hash (a SHA-2 digest, a fixed-length fingerprint of the file's contents) and a contentType, while the actual bytes travel as a separate part of the same HTTP request.

Extensions let a Learning Record Provider attach custom, application-specific data to a Statement's Result, Context, or Activity Definition, when none of the standard fields fit. Each Extension is a key-value pair where the key is an IRI (so two textbooks can each define an extension called, say, hintsUsed, without colliding) and the value can be any JSON data. A MicroSim tracking how many hints a student requested before answering, for instance, has no standard xAPI field for "hints used" — an Extension is exactly the escape hatch built for that case.

Before looking at how these pieces fit together on one Statement, it helps to see one more concept: a Registration is a UUID, placed in a Statement's Context, that groups together every Statement belonging to one attempt or session. If Maya restarts the Photosynthesis Quiz twice, each attempt gets its own Registration, so a report can distinguish "her three Statements from attempt one" from "her five Statements from attempt two," even though both sets share the same Actor and the same Object.

The following JSON fragment shows Sub-Statement, Attachment, Extensions, and Registration each appearing in one realistic, if unusually rich, Statement. Read it after the definitions above rather than before — every field name in it was just introduced in prose.

{
  "id": "abc-123",
  "actor": { "account": { "homePage": "https://sis.example-district.org", "name": "student-4471" } },
  "verb": { "id": "http://adlnet.gov/expapi/verbs/recommended" },
  "object": {
    "objectType": "SubStatement",
    "actor": { "account": { "homePage": "https://sis.example-district.org", "name": "student-4471" } },
    "verb": { "id": "http://adlnet.gov/expapi/verbs/practiced" },
    "object": { "id": "https://lrs.example.org/activities/sine-wave-sim" }
  },
  "context": {
    "registration": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "extensions": { "https://lrs.example.org/ext/hintsUsed": 2 }
  },
  "attachments": [
    { "usageType": "https://lrs.example.org/attachment/screenshot", "contentType": "image/png", "sha2": "f7a3...", "length": 48213 }
  ]
}

The diagram below turns this same JSON shape into a clickable, labeled anatomy so each optional piece can be explored on its own.

Diagram: Anatomy of an Extended Statement

Anatomy of an Extended Statement

Type: infographic sim-id: extended-statement-anatomy
Library: p5.js
Status: Specified Template: https://github.com/dmccreary/xapi-course/tree/main/docs/sims/xapi-statement-builder

Bloom Taxonomy: Understand (L2) Bloom Taxonomy Verb: classify, differentiate

Learning objective: Let the learner classify each of the four optional Statement pieces (Sub-Statement, Attachment, Extensions, Registration) by clicking on it and seeing what problem it solves, reinforcing the JSON example above with a visual, exploratory layout.

Layout: A central "core Statement" box (Actor/Verb/Object, dimmed to signal "already learned in Chapter 1") surrounded by four labeled satellite boxes: "Sub-Statement" (attached to the Object), "Attachment" (attached to the Statement as a whole), "Extensions" (attached to Context or Result), and "Registration" (attached to Context).

Data Visibility Requirements: Stage 1: Show the dimmed core Statement (Actor, Verb, Object) with a caption "You already know this part." Stage 2: On click of "Sub-Statement," highlight the connection to Object and show the nested Actor-Verb-Object nesting from the worked example (Ms. Alvarez recommended Maya's practice). Stage 3: On click of "Attachment," show the SHA-2 hash reference and contentType from the worked example, with a note that the actual file bytes travel separately. Stage 4: On click of "Extensions," show the hintsUsed key-value example and emphasize the IRI-as-key pattern that prevents naming collisions. Stage 5: On click of "Registration," show two example Statements sharing one registration UUID versus two Statements with different UUIDs, to make the grouping concrete.

Interactive features: Each of the four satellite boxes is clickable, revealing its Stage content in a side panel without navigating away from the diagram. A "Reset" button collapses all panels back to Stage 1.

Instructional Rationale: A click-to-reveal exploratory layout is appropriate for this Understand-level, classify/differentiate objective because the four pieces are independent of one another — a linear step-through would falsely imply an order or dependency between them that the specification does not require.

Implementation: p5.js canvas with clickable regions, following the interaction pattern of the referenced template MicroSim.

Sending Statements Over the Web

Everything so far describes what a Statement contains. The rest of this chapter covers how a Statement actually gets from a Learning Record Provider's code to a Learning Record Store's storage — and how a report later pulls Statements back out.

xAPI's transport is a RESTful API — an architectural style for web APIs, built on ordinary HTTP, where every piece of data the API manages (a Statement, an Activity, an Agent) is treated as a resource with its own address, and a small, fixed set of standard operations act on that address rather than each resource inventing its own custom operations. This is the same style used by the vast majority of modern web services, which is part of why xAPI was able to reuse existing web infrastructure — proxies, caches, firewalls, developer tooling — instead of inventing a new transport from scratch.

RESTful APIs express which operation to perform using an HTTP Verb: a word carried in every HTTP request that names the action being requested of a resource. Three HTTP Verbs matter for the Statement resource specifically:

HTTP Verb What It Does to /statements Interacts With Immutability How
GET Retrieve one Statement by ID, or query a filtered list of Statements Read-only; never changes stored data
PUT Store one new Statement at a client-supplied Statement ID Fails if that Statement ID already exists — the LRS refuses to overwrite, which is immutability enforced at the protocol level
POST Store one or more new Statements; the LRS assigns Statement IDs for any that omit one Always creates new records, never modifies existing ones

There is no DELETE for Statements

Rowan waving a caution flag Notice which HTTP Verb is missing from that table: DELETE. That is not an oversight — it is Statement Immutability, the rule from earlier in this chapter, enforced directly in the API's design. There is no way to ask an xAPI Endpoint to delete a Statement. If a Statement is wrong, you void it; you do not delete it.

The specific web address a Learning Record Store exposes for these operations is called an xAPI Endpoint — a base URL, such as https://lrs.example.org/xapi/, followed by resource paths like /statements, /activities, or /agents, that together form the complete set of operations an LRS makes available over HTTP. Every request a Learning Record Provider sends targets one of these paths using one of the HTTP Verbs above.

A GET request to /statements rarely wants every Statement ever stored — a teacher's dashboard might want only Maya's Statements about one specific quiz, or only Statements from the last 24 hours. An xAPI Endpoint supports this through a Statement Query Parameter: a name-value pair appended to the request URL that narrows which Statements a GET request returns. Commonly used parameters include:

  • agent — restrict results to Statements about one specific Actor
  • verb — restrict results to Statements using one specific Verb
  • activity — restrict results to Statements about one specific Object Activity
  • since and until — restrict results to a time window
  • limit — cap how many Statements come back in one response

The following diagram brings the endpoint, the verbs, and the query parameters together into one request/response picture.

Diagram: xAPI Endpoint and HTTP Verbs

xAPI Endpoint and HTTP Verbs

Type: workflow sim-id: xapi-endpoint-http-verbs
Library: Mermaid
Status: Specified

Bloom Taxonomy: Apply (L3) Bloom Taxonomy Verb: demonstrate, apply

Learning objective: Let the learner apply their knowledge of HTTP Verbs and Statement Query Parameters by tracing three concrete requests against one xAPI Endpoint and predicting what each returns.

Purpose: Show a base endpoint box https://lrs.example.org/xapi/ with three outgoing request arrows to a /statements resource box, each labeled with a full example request.

Requests to show:

  1. PUT /statements?statementId=abc-123 with a JSON body — labeled "Store one Statement at a known ID"
  2. POST /statements with a JSON array body — labeled "Store multiple Statements; LRS assigns IDs"
  3. GET /statements?agent={Maya's account}&verb=completed&since=2026-01-01 — labeled "Query Maya's completions since New Year"

Response box: A fourth box showing an abbreviated JSON response for request 3, containing a statements array with one entry and a more field for pagination.

Interactive features: Every request arrow and the response box are clickable via Mermaid click directives. Clicking a request arrow opens an infobox explaining which HTTP Verb is used and why. Clicking the response box explains the more field's role in paging through large result sets.

Color coding: GET requests in blue, PUT in teal, POST in green — consistent with a legend shown beside the diagram.

Implementation: Mermaid flowchart with click-to-infobox coverage on every node. Responsive width tracking the containing element.

Proving Who Is Allowed to Write

An xAPI Endpoint cannot accept Statements from just anyone — a Learning Record Store holding real student data needs to verify that the software making a request is actually authorized to write to (or read from) it. xAPI Endpoints support two authentication schemes for this.

Basic Authentication is the simpler of the two: the Learning Record Provider sends a username and password, combined and encoded, in every single HTTP request's Authorization header. It is straightforward to implement and works well for a Learning Record Provider that is a trusted, server-side system — this project's own ingestion gateway, for instance, authenticates upstream textbook publishers this way.

OAuth Authentication is a more involved, token-based scheme: rather than sending a password on every request, a Learning Record Provider first exchanges its credentials once for a short-lived access token, and then presents that token on subsequent requests. OAuth is the better fit when a Learning Record Provider runs partly in an untrusted environment — a browser-based MicroSim, for example, where embedding a permanent password in client-side code would expose it to anyone who opens the browser's developer tools.

Before comparing the two side by side, notice the shape of the trade-off: Basic Authentication is simpler to implement but exposes a long-lived credential on every request, while OAuth Authentication is more complex to implement but limits exposure to a token that expires and can be revoked independently of the underlying password.

Diagram: Authentication Scheme Comparison

Authentication Scheme Comparison

Type: infographic sim-id: authentication-scheme-comparison
Library: p5.js
Status: Specified Template: https://github.com/dmccreary/xapi-course/tree/main/docs/sims/authentication-scheme-comparison

Bloom Taxonomy: Evaluate (L5) Bloom Taxonomy Verb: assess, justify

Learning objective: Let the learner evaluate which authentication scheme fits a given Learning Record Provider scenario, justifying the choice against criteria introduced in the prose above.

Purpose: Adapt the referenced template (which compares three schemes) to compare only the two this chapter covers — Basic Authentication and OAuth Authentication — across the criteria that matter for an intelligent-textbook deployment.

Criteria (columns):

  • Credential exposure per request
  • Implementation effort
  • Revocation (can access be cut off without changing the underlying password?)
  • Best-fit Learning Record Provider type

Rows: Basic Authentication, OAuth Authentication

Interactive features: Clicking a cell opens an infobox with a one-sentence justification tied back to the chapter's worked examples (server-side ingestion gateway for Basic; browser-based MicroSim for OAuth). A toggle lets the learner select a scenario ("server-side textbook publisher" or "browser-based MicroSim") and highlights the recommended scheme's row.

Instructional Rationale: An Evaluate-level objective calls for a tool that supports judgment against explicit criteria rather than passive viewing — the scenario toggle asks the learner to apply the criteria themselves before the tool confirms the recommendation.

Implementation: p5.js canvas adapted from the template's three-scheme comparison down to two schemes, with an added scenario-toggle interaction not present in the original.

Bringing It Together

This chapter completed the Statement started in Chapter 1. Every Statement now has an identity (Statement ID) and a guarantee that it cannot be silently rewritten (Statement Immutability), with Voided Statement as the sanctioned way to correct it. An Actor can be identified through an Actor Account, one of four kinds of Inverse Functional Identifier. A Statement can optionally nest another action as a Sub-Statement, carry binary data as an Attachment, hold custom data as Extensions, and group with related Statements through a Registration. And every one of these Statements travels over a RESTful API, addressed at an xAPI Endpoint, using an HTTP Verb, filtered by a Statement Query Parameter, and authenticated with either OAuth Authentication or Basic Authentication.

The following list is a quick self-check before moving on:

  1. A Statement's identity and trustworthiness come from its Statement ID plus the immutability rule — corrections are new Statements, never edits.
  2. An Actor's identity comes from exactly one Inverse Functional Identifier, most often an Actor Account for this book's K-12 audience.
  3. Sub-Statement, Attachment, Extensions, and Registration each solve a different "the core five fields aren't enough" problem.
  4. GET, PUT, and POST are the only HTTP Verbs an xAPI Endpoint needs — there is deliberately no DELETE.
  5. Basic Authentication and OAuth Authentication trade implementation simplicity against exposure and revocability.

That was a lot of protocol detail

Rowan offering encouragement If HTTP Verbs and authentication schemes feel like a detour from "learning records," they're not — they're what makes a Learning Record Store something real software can actually talk to. You don't need to memorize every query parameter. You need to recognize that every mechanic in this chapter exists to protect one promise: the record, once written, tells the truth.

Key Takeaways

  • A Statement ID gives every Statement a permanent, globally unique name.
  • Statement Immutability means Statements are never edited or deleted after acceptance; a Voided Statement — a new Statement whose Verb is voided and whose Object is a StatementRef — is the only sanctioned correction mechanism.
  • An Actor Account (homePage + name) is one of four kinds of Inverse Functional Identifier that uniquely identify an Actor.
  • Sub-Statement, Attachment, Extensions, and Registration extend a Statement to nest actions, carry files, hold custom data, and group related Statements.
  • xAPI is transported as a RESTful API: resources at an xAPI Endpoint, acted on with HTTP Verbs (GET, PUT, POST — never DELETE), filtered with Statement Query Parameters.
  • An xAPI Endpoint is protected by Basic Authentication or OAuth Authentication, chosen based on how trusted the Learning Record Provider's environment is.

You can now read and send a real xAPI request

Rowan celebrating with confetti Between Chapters 1 and 2, you've gone from "what is a Statement" to knowing exactly how one is built, corrected, and transmitted. What does the evidence show? In Chapter 3, we step back and look at who governs all of this — the IEEE standardization of xAPI and its companion specification, cmi5.