xAPI Validation Best Practices
Invalid data is worse than missing data
A statement that never arrives is an obvious problem. Somebody notices, somebody fixes the integration. A statement that arrives slightly wrong is the expensive one: it sits in your store looking like every other record, it counts towards your totals, and it is discovered months later by whoever is preparing a compliance report and cannot make the numbers agree.
Almost every case of unusable learning data traces back to something that could have been caught in the first week of an integration. Not a difficult problem — a field name with a capital letter, a timestamp missing its timezone, a learner identified two different ways by two different systems. Individually trivial, and collectively the reason organisations distrust their own dashboards.
The cheapest place to catch all of it is before the statement is ever sent.
Check a statement in ten seconds
The xAPI Validator is a free, open-source tool for exactly this. Paste a statement in, and it tells you what the specification makes of it — no account, no setup, nothing installed.
It is most useful at four moments, and building the habit at each of them prevents most of what follows in this article:
- When you write your first statement by hand. Get one correct before you write the code that generates thousands.
- When a vendor or platform tells you it sends xAPI. Ask for a sample statement and check it yourself — the gap between claimed and actual conformance is wide.
- When a batch is rejected and you cannot see why. The validator judges the statement in isolation, which separates a malformed statement from a malformed request.
- When you change what your integration sends. A new extension, a new verb, a new activity structure — validate one before deploying the change.
The mistakes that actually cost you
In roughly the order they show up in real integrations, with what each one does to your data if it gets through.
| Mistake | What it costs you | How to catch it |
|---|---|---|
| A field name with the wrong capitalisation — Timestamp, Actor, Verb | Systems that tolerate it store a field nothing queries. Reports silently miss those statements. | Validate one statement per integration before shipping. A strict LRS names the exact correction. |
| An actor identified two ways — by email in one system, by account in another | One learner becomes two. Completion rates halve, and nobody can tell why a specific person's transcript is incomplete. | Agree one identifier scheme across every sender before the first statement is sent. |
| A timestamp without a timezone offset, or with the -00:00 offset | Statements sort unpredictably. Any report bounded by dates is wrong by up to a day at the edges. | Check the offset on a sample from every sender — they rarely all agree. |
| A verb IRI that is not a real IRI — completed rather than the full ADL identifier | Your verbs cannot be grouped, so 'how many people completed anything' becomes unanswerable. | Keep an approved verb list and validate against it in code review. |
| Activity IDs that change between releases | Historical statements detach from the activity they describe. Long-term trends break at the release boundary. | Treat activity IDs as permanent identifiers, not as URLs that can be reorganised. |
| Extension keys that are not IRIs you control | Two teams pick the same key for different data. Filters return a mix of both. | Namespace extension keys under a domain you own. |
| contextActivities keys invented locally — module, chapter, unit | The relationship is stored but means nothing to any other system, including your next LRS. | The specification defines four: parent, grouping, category, other. Map your structure onto them. |
| result.score.scaled outside -1 to 1 | Averages become meaningless, and a single bad score can dominate a cohort report. | Validate score ranges at the source; scaled is a fraction, not a percentage. |
| No registration on grouped attempts | Multiple attempts merge into one. 'Passed on the third try' is indistinguishable from 'passed first time'. | Issue a registration per attempt and send it on every statement in that attempt. |
Timestamps deserve their own paragraph
More reporting problems come from timestamps than from any other field, because they are the one field where a plausible-looking value can still be wrong.
Send a full ISO 8601 timestamp with an explicit offset, and use UTC if you have any choice in the matter. Include at least milliseconds — two statements in the same second, ordered wrongly, turn a sequence of learner actions into nonsense. Avoid the -00:00 offset entirely: the specification treats it as an unknown offset rather than as UTC, so anything sorting by it is guessing.
It is also worth understanding the difference between two dates on every statement. The timestamp is when the learning happened; the stored date is when the LRS received it. A mobile app that syncs a week of offline activity produces statements whose timestamps are last week and whose stored dates are today. Report on the timestamp when you mean learning, on the stored date when you mean data arrival, and never mix the two in the same chart.
Identify every learner exactly one way
This is the single most consequential decision in an xAPI integration, and it is usually made accidentally by whoever wires up the first system.
The specification allows several identifiers: an email address, a hashed email, an OpenID, or an account on a named system. Any of them works. What does not work is different senders choosing differently — your LMS using email while your mobile app uses an internal account identifier. Both are valid, both are accepted, and the same person now exists twice in every report.
Pick one scheme, write it down, and make it a requirement for every system you connect. If you have a choice, an account identifier on a system you control ages better than an email address, because people change employers and email addresses far more often than they change staff numbers.
Keep a vocabulary, not a convention
Verbs and activity identifiers are the vocabulary of your learning data. Left to individual integrations, you end up with four ways of saying 'finished' and no way to ask how many people finished anything.
Maintain an approved list of verbs — the ADL registry covers most of what organisations need — and a naming scheme for activity identifiers that will still make sense after the next site reorganisation. An activity identifier is a permanent name for a thing, not a link to a page. If the URL changes and the identifier follows it, you have quietly split one activity's history in two.
Test the integration, not just the statement
A valid statement is necessary and not sufficient. These are the checks worth running before a new sender goes live.
- Validate one statement of each type your integration produces — a start, a completion, a score, a failure.
- Send them to a staging Store first, then read them back and confirm they look the way you expect on the way out, not just on the way in.
- Deliberately send something malformed and read the error. Knowing what a rejection looks like, and that your code logs it, is worth more than never having seen one.
- Check the same learner's statements from two different senders resolve to one person.
- Run one real report against the staging data. Numbers on a dashboard expose problems that statement-level checks do not.
When a statement is rejected
Sereni LRS validates strictly and synchronously, before anything is stored: a malformed statement is refused rather than becoming a row you find later. Rejections name the field at fault, and for batches they name the index, so you know which record to fix rather than bisecting a file.
If the message points at a field and you still cannot see the problem — an invisible character, a subtly wrong nesting, a value that looks like a string and is not — paste the statement into the xAPI Validator for an independent reading. The exact rules and the error text for each are listed in the documentation.
A checklist before you go live
Nine items, all of which can be settled in an afternoon and none of which can be settled cheaply afterwards.
- One learner identifier scheme, documented, used by every sender.
- An approved verb list, and a reviewer who checks new verbs against it.
- Activity identifiers designed as permanent names.
- Extension keys namespaced under a domain you own.
- Full ISO 8601 timestamps with explicit offsets and at least milliseconds.
- A registration per attempt wherever attempts matter.
- One statement of each type validated before the integration ships.
- Rejection responses logged in full, with somebody watching them.
- A staging Store, so the first statement anyone sends is never sent to production.
Ready to Get More from Your Learning Data?
Discover how Sereni LRS helps you capture, understand, and act on learning data through enterprise-grade analytics, flexible reporting, and a modern xAPI experience.
See Sereni LRS in Action